From bb2c3d63398e4f1731b8b376319863f442e13fd6 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 12 Jul 2024 17:20:52 +0930 Subject: [PATCH 01/12] Convert Database --- .../securesms/database/Database.java | 111 ------------------ .../securesms/database/Database.kt | 108 +++++++++++++++++ 2 files changed, 108 insertions(+), 111 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/Database.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/Database.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java b/app/src/main/java/org/thoughtcrime/securesms/database/Database.java deleted file mode 100644 index e1879d52304..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Database.java +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.ContentObserver; -import android.database.Cursor; - -import androidx.annotation.NonNull; - -import net.zetetic.database.sqlcipher.SQLiteDatabase; - -import org.session.libsession.utilities.WindowDebouncer; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; - -import java.util.Set; - -public abstract class Database { - - protected static final String ID_WHERE = "_id = ?"; - protected static final String ID_IN = "_id IN (?)"; - - protected SQLCipherOpenHelper databaseHelper; - protected final Context context; - private final WindowDebouncer conversationListNotificationDebouncer; - private final Runnable conversationListUpdater; - - @SuppressLint("WrongConstant") - public Database(Context context, SQLCipherOpenHelper databaseHelper) { - this.context = context; - this.conversationListUpdater = () -> { - context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null); - }; - this.databaseHelper = databaseHelper; - this.conversationListNotificationDebouncer = ApplicationContext.getInstance(context).getConversationListDebouncer(); - } - - protected void notifyConversationListeners(Set threadIds) { - for (long threadId : threadIds) - notifyConversationListeners(threadId); - } - - protected void notifyConversationListeners(long threadId) { - ConversationNotificationDebouncer.Companion.get(context).notify(threadId); - } - - protected void notifyConversationListListeners() { - conversationListNotificationDebouncer.publish(conversationListUpdater); - } - - protected void notifyStickerListeners() { - context.getContentResolver().notifyChange(DatabaseContentProviders.Sticker.CONTENT_URI, null); - } - - protected void notifyStickerPackListeners() { - context.getContentResolver().notifyChange(DatabaseContentProviders.StickerPack.CONTENT_URI, null); - } - - protected void notifyRecipientListeners() { - context.getContentResolver().notifyChange(DatabaseContentProviders.Recipient.CONTENT_URI, null); - notifyConversationListListeners(); - } - - protected void setNotifyConversationListeners(Cursor cursor, long threadId) { - cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForThread(threadId)); - } - - protected void setNotifyConversationListListeners(Cursor cursor) { - cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI); - } - - protected void registerAttachmentListeners(@NonNull ContentObserver observer) { - context.getContentResolver().registerContentObserver(DatabaseContentProviders.Attachment.CONTENT_URI, - true, - observer); - } - - protected void notifyAttachmentListeners() { - context.getContentResolver().notifyChange(DatabaseContentProviders.Attachment.CONTENT_URI, null); - } - - public void reset(SQLCipherOpenHelper databaseHelper) { - this.databaseHelper = databaseHelper; - } - - protected SQLiteDatabase getReadableDatabase() { - return databaseHelper.getReadableDatabase(); - } - - protected SQLiteDatabase getWritableDatabase() { - return databaseHelper.getWritableDatabase(); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Database.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Database.kt new file mode 100644 index 00000000000..f51bc2101fb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Database.kt @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2011 Whisper Systems + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see //www.gnu.org/licenses/>. + */ +package org.thoughtcrime.securesms.database + +import android.annotation.SuppressLint +import android.content.Context +import android.database.ContentObserver +import android.database.Cursor +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.session.libsession.utilities.WindowDebouncer +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.database.ConversationNotificationDebouncer.Companion.get +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper + +abstract class Database @SuppressLint("WrongConstant") constructor( + @JvmField protected val context: Context, + @JvmField protected var databaseHelper: SQLCipherOpenHelper +) { + private val conversationListNotificationDebouncer: WindowDebouncer = + ApplicationContext.getInstance( + context + ).conversationListDebouncer + private val conversationListUpdater = Runnable { + context.contentResolver.notifyChange( + DatabaseContentProviders.ConversationList.CONTENT_URI, + null + ) + } + + protected fun notifyConversationListeners(threadIds: Set) { + for (threadId in threadIds) notifyConversationListeners(threadId) + } + + protected fun notifyConversationListeners(threadId: Long) { + get(context).notify(threadId) + } + + fun notifyConversationListListeners() { + conversationListNotificationDebouncer.publish(conversationListUpdater) + } + + protected fun notifyStickerListeners() { + context.contentResolver.notifyChange(DatabaseContentProviders.Sticker.CONTENT_URI, null) + } + + protected fun notifyStickerPackListeners() { + context.contentResolver.notifyChange(DatabaseContentProviders.StickerPack.CONTENT_URI, null) + } + + protected fun notifyRecipientListeners() { + context.contentResolver.notifyChange(DatabaseContentProviders.Recipient.CONTENT_URI, null) + notifyConversationListListeners() + } + + protected fun setNotifyConversationListeners(cursor: Cursor, threadId: Long) { + cursor.setNotificationUri( + context.contentResolver, + DatabaseContentProviders.Conversation.getUriForThread(threadId) + ) + } + + protected fun setNotifyConversationListListeners(cursor: Cursor) { + cursor.setNotificationUri( + context.contentResolver, + DatabaseContentProviders.ConversationList.CONTENT_URI + ) + } + + protected fun registerAttachmentListeners(observer: ContentObserver) { + context.contentResolver.registerContentObserver( + DatabaseContentProviders.Attachment.CONTENT_URI, + true, + observer + ) + } + + protected fun notifyAttachmentListeners() { + context.contentResolver.notifyChange(DatabaseContentProviders.Attachment.CONTENT_URI, null) + } + + fun reset(databaseHelper: SQLCipherOpenHelper) { + this.databaseHelper = databaseHelper + } + + protected val readableDatabase: SQLiteDatabase + get() = databaseHelper.readableDatabase + + protected val writableDatabase: SQLiteDatabase + get() = databaseHelper.writableDatabase + + companion object { + const val ID_WHERE: String = "_id = ?" + } +} From 14a909566b2287b2bb72f2d6da6faea21de4b72c Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 12 Jul 2024 17:28:58 +0930 Subject: [PATCH 02/12] Convert AttachmentDatabase --- .../database/AttachmentDatabase.java | 1003 --------------- .../securesms/database/AttachmentDatabase.kt | 1102 +++++++++++++++++ .../securesms/database/MmsDatabase.kt | 35 +- 3 files changed, 1116 insertions(+), 1024 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java deleted file mode 100644 index 45172e2f6fe..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ /dev/null @@ -1,1003 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database; - -import android.annotation.SuppressLint; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.os.Build; -import android.text.TextUtils; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.bumptech.glide.Glide; - -import net.zetetic.database.sqlcipher.SQLiteDatabase; - -import org.apache.commons.lang3.StringUtils; -import org.json.JSONArray; -import org.json.JSONException; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras; -import org.session.libsession.utilities.MediaTypes; -import org.session.libsession.utilities.Util; -import org.session.libsignal.utilities.ExternalStorageUtil; -import org.session.libsignal.utilities.JsonUtil; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.crypto.AttachmentSecret; -import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; -import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; -import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; -import org.thoughtcrime.securesms.mms.MediaStream; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.util.BitmapDecodingException; -import org.thoughtcrime.securesms.util.BitmapUtil; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; -import org.thoughtcrime.securesms.video.EncryptedMediaDataSource; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; - -import kotlin.jvm.Synchronized; - -public class AttachmentDatabase extends Database { - - private static final String TAG = AttachmentDatabase.class.getSimpleName(); - - public static final String TABLE_NAME = "part"; - public static final String ROW_ID = "_id"; - static final String ATTACHMENT_JSON_ALIAS = "attachment_json"; - public static final String MMS_ID = "mid"; - static final String CONTENT_TYPE = "ct"; - static final String NAME = "name"; - static final String CONTENT_DISPOSITION = "cd"; - static final String CONTENT_LOCATION = "cl"; - public static final String DATA = "_data"; - static final String TRANSFER_STATE = "pending_push"; - public static final String SIZE = "data_size"; - static final String FILE_NAME = "file_name"; - public static final String THUMBNAIL = "thumbnail"; - static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio"; - public static final String UNIQUE_ID = "unique_id"; - static final String DIGEST = "digest"; - static final String VOICE_NOTE = "voice_note"; - static final String QUOTE = "quote"; - public static final String STICKER_PACK_ID = "sticker_pack_id"; - public static final String STICKER_PACK_KEY = "sticker_pack_key"; - static final String STICKER_ID = "sticker_id"; - static final String FAST_PREFLIGHT_ID = "fast_preflight_id"; - public static final String DATA_RANDOM = "data_random"; - private static final String THUMBNAIL_RANDOM = "thumbnail_random"; - static final String WIDTH = "width"; - static final String HEIGHT = "height"; - static final String CAPTION = "caption"; - public static final String URL = "url"; - public static final String DIRECTORY = "parts"; - // "audio/*" mime type only related columns. - static final String AUDIO_VISUAL_SAMPLES = "audio_visual_samples"; // Small amount of audio byte samples to visualise the content (e.g. draw waveform). - static final String AUDIO_DURATION = "audio_duration"; // Duration of the audio track in milliseconds. - - private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?"; - private static final String ROW_ID_WHERE = ROW_ID + " = ?"; - private static final String PART_AUDIO_ONLY_WHERE = CONTENT_TYPE + " LIKE \"audio/%\""; - - private static final String[] PROJECTION = new String[] {ROW_ID, - MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, - CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE, - SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO, - UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, - QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT, - CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID, URL}; - - private static final String[] PROJECTION_AUDIO_EXTRAS = new String[] {AUDIO_VISUAL_SAMPLES, AUDIO_DURATION}; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + - MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " + - CONTENT_TYPE + " TEXT, " + NAME + " TEXT, " + "chset" + " INTEGER, " + - CONTENT_DISPOSITION + " TEXT, " + "fn" + " TEXT, " + "cid" + " TEXT, " + - CONTENT_LOCATION + " TEXT, " + "ctt_s" + " INTEGER, " + - "ctt_t" + " TEXT, " + "encrypted" + " INTEGER, " + - TRANSFER_STATE + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " + - FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " + - UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " + - VOICE_NOTE + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + THUMBNAIL_RANDOM + " BLOB, " + - QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0, " + - CAPTION + " TEXT DEFAULT NULL, " + URL + " TEXT, " + STICKER_PACK_ID + " TEXT DEFAULT NULL, " + - STICKER_PACK_KEY + " DEFAULT NULL, " + STICKER_ID + " INTEGER DEFAULT -1," + - AUDIO_VISUAL_SAMPLES + " BLOB, " + AUDIO_DURATION + " INTEGER);"; - - public static final String[] CREATE_INDEXS = { - "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", - "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + TRANSFER_STATE + ");", - "CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON " + TABLE_NAME + " (" + STICKER_PACK_ID + ");", - }; - - private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor(); - - private final AttachmentSecret attachmentSecret; - - public AttachmentDatabase(Context context, SQLCipherOpenHelper databaseHelper, AttachmentSecret attachmentSecret) { - super(context, databaseHelper); - this.attachmentSecret = attachmentSecret; - } - - public @NonNull InputStream getAttachmentStream(AttachmentId attachmentId, long offset) - throws IOException - { - InputStream dataStream = getDataStream(attachmentId, DATA, offset); - - if (dataStream == null) throw new IOException("No stream for: " + attachmentId); - else return dataStream; - } - - public @NonNull InputStream getThumbnailStream(@NonNull AttachmentId attachmentId) - throws IOException - { - Log.d(TAG, "getThumbnailStream(" + attachmentId + ")"); - InputStream dataStream = getDataStream(attachmentId, THUMBNAIL, 0); - - if (dataStream != null) { - return dataStream; - } - - try { - InputStream generatedStream = thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)).get(); - - if (generatedStream == null) throw new FileNotFoundException("No thumbnail stream available: " + attachmentId); - else return generatedStream; - } catch (InterruptedException ie) { - throw new AssertionError("interrupted"); - } catch (ExecutionException ee) { - Log.w(TAG, ee); - throw new IOException(ee); - } - } - - public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) - throws MmsException - { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED); - - database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); - notifyConversationListeners(DatabaseComponent.get(context).mmsDatabase().getThreadIdForMessage(mmsId)); - } - - public @Nullable DatabaseAttachment getAttachment(@NonNull AttachmentId attachmentId) - { - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = null; - - try { - cursor = database.query(TABLE_NAME, PROJECTION, ROW_ID_WHERE, new String[]{String.valueOf(attachmentId.getRowId())}, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - List list = getAttachment(cursor); - - if (list != null && list.size() > 0) { - return list.get(0); - } - } - - return null; - } finally { - if (cursor != null) - cursor.close(); - } - } - - public @NonNull List getAttachmentsForMessage(long mmsId) { - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - List results = new LinkedList<>(); - Cursor cursor = null; - - try { - cursor = database.query(TABLE_NAME, PROJECTION, MMS_ID + " = ?", new String[] {mmsId+""}, - null, null, null); - - while (cursor != null && cursor.moveToNext()) { - List attachments = getAttachment(cursor); - for (DatabaseAttachment attachment : attachments) { - if (attachment.isQuote()) continue; - results.add(attachment); - } - } - - return results; - } finally { - if (cursor != null) - cursor.close(); - } - } - - public @NonNull List getPendingAttachments() { - final SQLiteDatabase database = databaseHelper.getReadableDatabase(); - final List attachments = new LinkedList<>(); - - Cursor cursor = null; - try { - cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED)}, null, null, null); - while (cursor != null && cursor.moveToNext()) { - attachments.addAll(getAttachment(cursor)); - } - } finally { - if (cursor != null) cursor.close(); - } - - return attachments; - } - - void deleteAttachmentsForMessages(String[] messageIds) { - StringBuilder queryBuilder = new StringBuilder(); - for (int i = 0; i < messageIds.length; i++) { - queryBuilder.append(MMS_ID+" = ").append(messageIds[i]); - if (i+1 < messageIds.length) { - queryBuilder.append(" OR "); - } - } - String idsAsString = queryBuilder.toString(); - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = null; - List attachmentInfos = new ArrayList<>(); - try { - cursor = database.query(TABLE_NAME, new String[] { DATA, THUMBNAIL, CONTENT_TYPE}, idsAsString, null, null, null, null); - while (cursor != null && cursor.moveToNext()) { - attachmentInfos.add(new MmsAttachmentInfo(cursor.getString(0), cursor.getString(1), cursor.getString(2))); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - deleteAttachmentsOnDisk(attachmentInfos); - database.delete(TABLE_NAME, idsAsString, null); - notifyAttachmentListeners(); - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - void deleteAttachmentsForMessage(long mmsId) { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - Cursor cursor = null; - - try { - cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE}, MMS_ID + " = ?", - new String[] {mmsId+""}, null, null, null); - - while (cursor != null && cursor.moveToNext()) { - deleteAttachmentOnDisk(cursor.getString(0), cursor.getString(1), cursor.getString(2)); - } - } finally { - if (cursor != null) - cursor.close(); - } - - database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {mmsId + ""}); - notifyAttachmentListeners(); - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - void deleteAttachmentsForMessages(long[] mmsIds) { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - Cursor cursor = null; - String mmsIdString = StringUtils.join(mmsIds, ','); - - try { - cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE}, MMS_ID + " IN (?)", - new String[] {mmsIdString}, null, null, null); - - while (cursor != null && cursor.moveToNext()) { - deleteAttachmentOnDisk(cursor.getString(0), cursor.getString(1), cursor.getString(2)); - } - } finally { - if (cursor != null) - cursor.close(); - } - - database.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {mmsIdString}); - notifyAttachmentListeners(); - } - - public void deleteAttachment(@NonNull AttachmentId id) { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - - try (Cursor cursor = database.query(TABLE_NAME, - new String[]{DATA, THUMBNAIL, CONTENT_TYPE}, - PART_ID_WHERE, - id.toStrings(), - null, - null, - null)) - { - if (cursor == null || !cursor.moveToNext()) { - Log.w(TAG, "Tried to delete an attachment, but it didn't exist."); - return; - } - String data = cursor.getString(0); - String thumbnail = cursor.getString(1); - String contentType = cursor.getString(2); - - database.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings()); - deleteAttachmentOnDisk(data, thumbnail, contentType); - notifyAttachmentListeners(); - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - void deleteAllAttachments() { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - database.delete(TABLE_NAME, null, null); - - File attachmentsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); - File[] attachments = attachmentsDirectory.listFiles(); - - for (File attachment : attachments) { - attachment.delete(); - } - - notifyAttachmentListeners(); - } - - private void deleteAttachmentsOnDisk(List mmsAttachmentInfos) { - for (MmsAttachmentInfo info : mmsAttachmentInfos) { - if (info.getDataFile() != null && !TextUtils.isEmpty(info.getDataFile())) { - File data = new File(info.getDataFile()); - if (data.exists()) { - data.delete(); - } - } - if (info.getThumbnailFile() != null && !TextUtils.isEmpty(info.getThumbnailFile())) { - File thumbnail = new File(info.getThumbnailFile()); - if (thumbnail.exists()) { - thumbnail.delete(); - } - } - } - - boolean anyImageType = MmsAttachmentInfo.anyImages(mmsAttachmentInfos); - boolean anyThumbnail = MmsAttachmentInfo.anyThumbnailNonNull(mmsAttachmentInfos); - - if (anyImageType || anyThumbnail) { - Glide.get(context).clearDiskCache(); - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - private void deleteAttachmentOnDisk(@Nullable String data, @Nullable String thumbnail, @Nullable String contentType) { - if (!TextUtils.isEmpty(data)) { - new File(data).delete(); - } - - if (!TextUtils.isEmpty(thumbnail)) { - new File(thumbnail).delete(); - } - - if (MediaUtil.isImageType(contentType) || thumbnail != null) { - Glide.get(context).clearDiskCache(); - } - } - - public void insertAttachmentsForPlaceholder(long mmsId, @NonNull AttachmentId attachmentId, @NonNull InputStream inputStream) - throws MmsException - { - DatabaseAttachment placeholder = getAttachment(attachmentId); - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - ContentValues values = new ContentValues(); - DataInfo dataInfo = setAttachmentData(inputStream); - - if (placeholder != null && placeholder.isQuote() && !placeholder.getContentType().startsWith("image")) { - values.put(THUMBNAIL, dataInfo.file.getAbsolutePath()); - values.put(THUMBNAIL_RANDOM, dataInfo.random); - } else { - values.put(DATA, dataInfo.file.getAbsolutePath()); - values.put(SIZE, dataInfo.length); - values.put(DATA_RANDOM, dataInfo.random); - } - - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); - values.put(CONTENT_LOCATION, (String)null); - values.put(CONTENT_DISPOSITION, (String)null); - values.put(DIGEST, (byte[])null); - values.put(NAME, (String) null); - values.put(FAST_PREFLIGHT_ID, (String)null); - values.put(URL, ""); - - if (database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) { - //noinspection ResultOfMethodCallIgnored - dataInfo.file.delete(); - } else { - notifyConversationListeners(DatabaseComponent.get(context).mmsDatabase().getThreadIdForMessage(mmsId)); - notifyConversationListListeners(); - } - - thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)); - } - - public void updateAttachmentAfterUploadSucceeded(@NonNull AttachmentId id, @NonNull Attachment attachment) { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - ContentValues values = new ContentValues(); - - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); - values.put(CONTENT_LOCATION, attachment.getLocation()); - values.put(DIGEST, attachment.getDigest()); - values.put(CONTENT_DISPOSITION, attachment.getKey()); - values.put(NAME, attachment.getRelay()); - values.put(SIZE, attachment.getSize()); - values.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId()); - values.put(URL, attachment.getUrl()); - - database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); - } - - public void handleFailedAttachmentUpload(@NonNull AttachmentId id) { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - ContentValues values = new ContentValues(); - - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED); - - database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); - } - - @NonNull Map insertAttachmentsForMessage(long mmsId, @NonNull List attachments, @NonNull List quoteAttachment) - throws MmsException - { - Log.d(TAG, "insertParts(" + attachments.size() + ")"); - - Map insertedAttachments = new HashMap<>(); - - for (Attachment attachment : attachments) { - AttachmentId attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote()); - insertedAttachments.put(attachment, attachmentId); - Log.i(TAG, "Inserted attachment at ID: " + attachmentId); - } - - for (Attachment attachment : quoteAttachment) { - AttachmentId attachmentId = insertAttachment(mmsId, attachment, true); - insertedAttachments.put(attachment, attachmentId); - Log.i(TAG, "Inserted quoted attachment at ID: " + attachmentId); - } - - return insertedAttachments; - } - - /** - * Insert attachments in database and return the IDs of the inserted attachments - * - * @param mmsId message ID - * @param attachments attachments to persist - * @return IDs of the persisted attachments - * @throws MmsException - */ - @NonNull List insertAttachments(long mmsId, @NonNull List attachments) - throws MmsException - { - Log.d(TAG, "insertParts(" + attachments.size() + ")"); - - List insertedAttachmentsIDs = new LinkedList<>(); - - for (Attachment attachment : attachments) { - AttachmentId attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote()); - insertedAttachmentsIDs.add(attachmentId.getRowId()); - Log.i(TAG, "Inserted attachment at ID: " + attachmentId); - } - - return insertedAttachmentsIDs; - } - - public @NonNull Attachment updateAttachmentData(@NonNull Attachment attachment, - @NonNull MediaStream mediaStream) - throws MmsException - { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - DatabaseAttachment databaseAttachment = (DatabaseAttachment) attachment; - DataInfo dataInfo = getAttachmentDataFileInfo(databaseAttachment.getAttachmentId(), DATA); - - if (dataInfo == null) { - throw new MmsException("No attachment data found!"); - } - - dataInfo = setAttachmentData(dataInfo.file, mediaStream.getStream()); - - ContentValues contentValues = new ContentValues(); - contentValues.put(SIZE, dataInfo.length); - contentValues.put(CONTENT_TYPE, mediaStream.getMimeType()); - contentValues.put(WIDTH, mediaStream.getWidth()); - contentValues.put(HEIGHT, mediaStream.getHeight()); - contentValues.put(DATA_RANDOM, dataInfo.random); - - database.update(TABLE_NAME, contentValues, PART_ID_WHERE, databaseAttachment.getAttachmentId().toStrings()); - - return new DatabaseAttachment(databaseAttachment.getAttachmentId(), - databaseAttachment.getMmsId(), - databaseAttachment.hasData(), - databaseAttachment.hasThumbnail(), - mediaStream.getMimeType(), - databaseAttachment.getTransferState(), - dataInfo.length, - databaseAttachment.getFileName(), - databaseAttachment.getLocation(), - databaseAttachment.getKey(), - databaseAttachment.getRelay(), - databaseAttachment.getDigest(), - databaseAttachment.getFastPreflightId(), - databaseAttachment.isVoiceNote(), - mediaStream.getWidth(), - mediaStream.getHeight(), - databaseAttachment.isQuote(), - databaseAttachment.getCaption(), - databaseAttachment.getUrl()); - } - - public void markAttachmentUploaded(long messageId, Attachment attachment) { - ContentValues values = new ContentValues(1); - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); - database.update(TABLE_NAME, values, PART_ID_WHERE, ((DatabaseAttachment)attachment).getAttachmentId().toStrings()); - - notifyConversationListeners(DatabaseComponent.get(context).mmsDatabase().getThreadIdForMessage(messageId)); - ((DatabaseAttachment) attachment).setUploaded(true); - } - - public void setTransferState(long messageId, @NonNull AttachmentId attachmentId, int transferState) { - final ContentValues values = new ContentValues(1); - final SQLiteDatabase database = databaseHelper.getWritableDatabase(); - - values.put(TRANSFER_STATE, transferState); - database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); - notifyConversationListeners(DatabaseComponent.get(context).mmsDatabase().getThreadIdForMessage(messageId)); - } - - @SuppressWarnings("WeakerAccess") - @VisibleForTesting - protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType, long offset) - { - DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, dataType); - - if (dataInfo == null) { - return null; - } - - try { - if (dataInfo.random != null && dataInfo.random.length == 32) { - return ModernDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.random, dataInfo.file, offset); - } else { - InputStream stream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.file); - long skipped = stream.skip(offset); - - if (skipped != offset) { - Log.w(TAG, "Skip failed: " + skipped + " vs " + offset); - return null; - } - - return stream; - } - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } - - private @Nullable DataInfo getAttachmentDataFileInfo(@NonNull AttachmentId attachmentId, @NonNull String dataType) - { - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = null; - - String randomColumn; - - switch (dataType) { - case DATA: randomColumn = DATA_RANDOM; break; - case THUMBNAIL: randomColumn = THUMBNAIL_RANDOM; break; - default:throw new AssertionError("Unknown data type: " + dataType); - } - - try { - cursor = database.query(TABLE_NAME, new String[]{dataType, SIZE, randomColumn}, PART_ID_WHERE, attachmentId.toStrings(), - null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - if (cursor.isNull(0)) { - return null; - } - - return new DataInfo(new File(cursor.getString(0)), - cursor.getLong(1), - cursor.getBlob(2)); - } else { - return null; - } - } finally { - if (cursor != null) - cursor.close(); - } - - } - - private @NonNull DataInfo setAttachmentData(@NonNull Uri uri) - throws MmsException - { - try { - InputStream inputStream = PartAuthority.getAttachmentStream(context, uri); - return setAttachmentData(inputStream); - } catch (IOException e) { - throw new MmsException(e); - } - } - - private @NonNull DataInfo setAttachmentData(@NonNull InputStream in) - throws MmsException - { - try { - File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); - File dataFile = File.createTempFile("part", ".mms", partsDirectory); - return setAttachmentData(dataFile, in); - } catch (IOException e) { - throw new MmsException(e); - } - } - - private @NonNull DataInfo setAttachmentData(@NonNull File destination, @NonNull InputStream in) - throws MmsException - { - try { - Pair out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, destination, false); - long length = Util.copy(in, out.second); - - return new DataInfo(destination, length, out.first); - } catch (IOException e) { - throw new MmsException(e); - } - } - - public List getAttachment(@NonNull Cursor cursor) { - try { - if (cursor.getColumnIndex(AttachmentDatabase.ATTACHMENT_JSON_ALIAS) != -1) { - if (cursor.isNull(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) { - return new LinkedList<>(); - } - - Set result = new TreeSet<>((o1, o2) -> o1.getAttachmentId().equals(o2.getAttachmentId()) ? 0 : 1); - JSONArray array = new JSONArray(cursor.getString(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))); - - for (int i=0;i(result); - } else { - int urlIndex = cursor.getColumnIndex(URL); - return Collections.singletonList(new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), - cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))), - cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), - !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), - !cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), - cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), - cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), - cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), - cursor.getString(cursor.getColumnIndexOrThrow(NAME)), - cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)), - cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), - cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, - cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), - cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)), - cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1, - cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)), - urlIndex > 0 ? cursor.getString(urlIndex) : "")); - } - } catch (JSONException e) { - throw new AssertionError(e); - } - } - - - private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean quote) - throws MmsException - { - Log.d(TAG, "Inserting attachment for mms id: " + mmsId); - - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - DataInfo dataInfo = null; - long uniqueId = System.currentTimeMillis(); - - if (attachment.getDataUri() != null) { - dataInfo = setAttachmentData(attachment.getDataUri()); - Log.d(TAG, "Wrote part to file: " + dataInfo.file.getAbsolutePath()); - } - - ContentValues contentValues = new ContentValues(); - contentValues.put(MMS_ID, mmsId); - contentValues.put(CONTENT_TYPE, attachment.getContentType()); - contentValues.put(TRANSFER_STATE, attachment.getTransferState()); - contentValues.put(UNIQUE_ID, uniqueId); - contentValues.put(CONTENT_LOCATION, attachment.getLocation()); - contentValues.put(DIGEST, attachment.getDigest()); - contentValues.put(CONTENT_DISPOSITION, attachment.getKey()); - contentValues.put(NAME, attachment.getRelay()); - contentValues.put(FILE_NAME, ExternalStorageUtil.getCleanFileName(attachment.getFileName())); - contentValues.put(SIZE, attachment.getSize()); - contentValues.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId()); - contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0); - contentValues.put(WIDTH, attachment.getWidth()); - contentValues.put(HEIGHT, attachment.getHeight()); - contentValues.put(QUOTE, quote); - contentValues.put(CAPTION, attachment.getCaption()); - contentValues.put(URL, attachment.getUrl()); - - if (dataInfo != null) { - contentValues.put(DATA, dataInfo.file.getAbsolutePath()); - contentValues.put(SIZE, dataInfo.length); - contentValues.put(DATA_RANDOM, dataInfo.random); - } - - long rowId = database.insert(TABLE_NAME, null, contentValues); - AttachmentId attachmentId = new AttachmentId(rowId, uniqueId); - Uri thumbnailUri = attachment.getThumbnailUri(); - boolean hasThumbnail = false; - - if (thumbnailUri != null) { - try (InputStream attachmentStream = PartAuthority.getAttachmentStream(context, thumbnailUri)) { - Pair dimens; - if (attachment.getContentType().equals(MediaTypes.IMAGE_GIF)) { - dimens = new Pair<>(attachment.getWidth(), attachment.getHeight()); - } else { - dimens = BitmapUtil.getDimensions(attachmentStream); - } - updateAttachmentThumbnail(attachmentId, - PartAuthority.getAttachmentStream(context, thumbnailUri), - (float) dimens.first / (float) dimens.second); - hasThumbnail = true; - } catch (IOException | BitmapDecodingException e) { - Log.w(TAG, "Failed to save existing thumbnail.", e); - } - } - - if (!hasThumbnail && dataInfo != null) { - if (MediaUtil.hasVideoThumbnail(attachment.getDataUri())) { - Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getDataUri()); - - if (bitmap != null) { - ThumbnailData thumbnailData = new ThumbnailData(bitmap); - updateAttachmentThumbnail(attachmentId, thumbnailData.toDataStream(), thumbnailData.getAspectRatio()); - } else { - Log.w(TAG, "Retrieving video thumbnail failed, submitting thumbnail generation job..."); - thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)); - } - } else { - Log.i(TAG, "Submitting thumbnail generation job..."); - thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)); - } - } - - return attachmentId; - } - - @SuppressWarnings("WeakerAccess") - @VisibleForTesting - protected void updateAttachmentThumbnail(AttachmentId attachmentId, InputStream in, float aspectRatio) - throws MmsException - { - Log.i(TAG, "updating part thumbnail for #" + attachmentId); - - DataInfo thumbnailFile = setAttachmentData(in); - - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - ContentValues values = new ContentValues(2); - - values.put(THUMBNAIL, thumbnailFile.file.getAbsolutePath()); - values.put(THUMBNAIL_ASPECT_RATIO, aspectRatio); - values.put(THUMBNAIL_RANDOM, thumbnailFile.random); - - database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); - - Cursor cursor = database.query(TABLE_NAME, new String[] {MMS_ID}, PART_ID_WHERE, attachmentId.toStrings(), null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - notifyConversationListeners(DatabaseComponent.get(context).mmsDatabase().getThreadIdForMessage(cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)))); - } - } finally { - if (cursor != null) cursor.close(); - } - } - - /** - * Retrieves the audio extra values associated with the attachment. Only "audio/*" mime type attachments are accepted. - * @return the related audio extras or null in case any of the audio extra columns are empty or the attachment is not an audio. - */ - @Synchronized - public @Nullable DatabaseAttachmentAudioExtras getAttachmentAudioExtras(@NonNull AttachmentId attachmentId) { - try (Cursor cursor = databaseHelper.getReadableDatabase() - // We expect all the audio extra values to be present (not null) or reject the whole record. - .query(TABLE_NAME, - PROJECTION_AUDIO_EXTRAS, - PART_ID_WHERE + - " AND " + AUDIO_VISUAL_SAMPLES + " IS NOT NULL" + - " AND " + AUDIO_DURATION + " IS NOT NULL" + - " AND " + PART_AUDIO_ONLY_WHERE, - attachmentId.toStrings(), - null, null, null, "1")) { - - if (cursor == null || !cursor.moveToFirst()) return null; - - byte[] audioSamples = cursor.getBlob(cursor.getColumnIndexOrThrow(AUDIO_VISUAL_SAMPLES)); - long duration = cursor.getLong(cursor.getColumnIndexOrThrow(AUDIO_DURATION)); - - return new DatabaseAttachmentAudioExtras(attachmentId, audioSamples, duration); - } - } - - /** - * Updates audio extra columns for the "audio/*" mime type attachments only. - * @return true if the update operation was successful. - */ - @Synchronized - public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras, long threadId) { - ContentValues values = new ContentValues(); - values.put(AUDIO_VISUAL_SAMPLES, extras.getVisualSamples()); - values.put(AUDIO_DURATION, extras.getDurationMs()); - - int alteredRows = databaseHelper.getWritableDatabase().update(TABLE_NAME, - values, - PART_ID_WHERE + " AND " + PART_AUDIO_ONLY_WHERE, - extras.getAttachmentId().toStrings()); - - if (threadId >= 0) { - notifyConversationListeners(threadId); - } - - return alteredRows > 0; - } - - /** - * Updates audio extra columns for the "audio/*" mime type attachments only. - * @return true if the update operation was successful. - */ - @Synchronized - public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras extras) { - return setAttachmentAudioExtras(extras, -1); // -1 for no update - } - - @VisibleForTesting - class ThumbnailFetchCallable implements Callable { - - private final AttachmentId attachmentId; - - ThumbnailFetchCallable(AttachmentId attachmentId) { - this.attachmentId = attachmentId; - } - - @Override - public @Nullable InputStream call() throws Exception { - Log.d(TAG, "Executing thumbnail job..."); - final InputStream stream = getDataStream(attachmentId, THUMBNAIL, 0); - - if (stream != null) { - return stream; - } - - DatabaseAttachment attachment = getAttachment(attachmentId); - - if (attachment == null || !attachment.hasData()) { - return null; - } - - ThumbnailData data = null; - - if (MediaUtil.isVideoType(attachment.getContentType())) { - data = generateVideoThumbnail(attachmentId); - } - - if (data == null) { - return null; - } - - updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.getAspectRatio()); - - return getDataStream(attachmentId, THUMBNAIL, 0); - } - - @SuppressLint("NewApi") - private ThumbnailData generateVideoThumbnail(AttachmentId attachmentId) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - Log.w(TAG, "Video thumbnails not supported..."); - return null; - } - - DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA); - - if (dataInfo == null) { - Log.w(TAG, "No data file found for video thumbnail..."); - return null; - } - - EncryptedMediaDataSource dataSource = new EncryptedMediaDataSource(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length); - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - retriever.setDataSource(dataSource); - - Bitmap bitmap = retriever.getFrameAtTime(1000); - - Log.i(TAG, "Generated video thumbnail..."); - return new ThumbnailData(bitmap); - } - } - - private static class DataInfo { - private final File file; - private final long length; - private final byte[] random; - - private DataInfo(File file, long length, byte[] random) { - this.file = file; - this.length = length; - this.random = random; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt new file mode 100644 index 00000000000..b3d1e3500bb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt @@ -0,0 +1,1102 @@ +/* + * Copyright (C) 2011 Whisper Systems + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.database + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Build +import android.text.TextUtils +import android.util.Pair +import androidx.annotation.VisibleForTesting +import com.bumptech.glide.Glide +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.apache.commons.lang3.StringUtils +import org.json.JSONArray +import org.json.JSONException +import org.session.libsession.messaging.sending_receiving.attachments.Attachment +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras +import org.session.libsession.utilities.MediaTypes +import org.session.libsession.utilities.Util.copy +import org.session.libsession.utilities.Util.newSingleThreadedLifoExecutor +import org.session.libsignal.utilities.ExternalStorageUtil.getCleanFileName +import org.session.libsignal.utilities.JsonUtil.SaneJSONObject +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.crypto.AttachmentSecret +import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream +import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream +import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo +import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo.Companion.anyImages +import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo.Companion.anyThumbnailNonNull +import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get +import org.thoughtcrime.securesms.mms.MediaStream +import org.thoughtcrime.securesms.mms.MmsException +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.util.BitmapDecodingException +import org.thoughtcrime.securesms.util.BitmapUtil +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData +import org.thoughtcrime.securesms.video.EncryptedMediaDataSource +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.util.LinkedList +import java.util.TreeSet +import java.util.concurrent.Callable +import java.util.concurrent.ExecutionException + +class AttachmentDatabase( + context: Context?, + databaseHelper: SQLCipherOpenHelper?, + private val attachmentSecret: AttachmentSecret +) : Database( + context!!, databaseHelper!! +) { + private val thumbnailExecutor = newSingleThreadedLifoExecutor() + + @Throws(IOException::class) + fun getAttachmentStream(attachmentId: AttachmentId, offset: Long): InputStream { + val dataStream = getDataStream(attachmentId, DATA, offset) + + if (dataStream == null) throw IOException("No stream for: $attachmentId") + else return dataStream + } + + @Throws(IOException::class) + fun getThumbnailStream(attachmentId: AttachmentId): InputStream { + Log.d(TAG, "getThumbnailStream($attachmentId)") + val dataStream = getDataStream(attachmentId, THUMBNAIL, 0) + + if (dataStream != null) { + return dataStream + } + + try { + val generatedStream = + thumbnailExecutor.submit(ThumbnailFetchCallable(attachmentId)).get() + + if (generatedStream == null) throw FileNotFoundException("No thumbnail stream available: $attachmentId") + else return generatedStream + } catch (ie: InterruptedException) { + throw AssertionError("interrupted") + } catch (ee: ExecutionException) { + Log.w(TAG, ee) + throw IOException(ee) + } + } + + @Throws(MmsException::class) + fun setTransferProgressFailed(attachmentId: AttachmentId, mmsId: Long) { + val database: SQLiteDatabase = databaseHelper.writableDatabase + val values = ContentValues() + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) + + database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) + notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(mmsId)) + } + + fun getAttachment(attachmentId: AttachmentId): DatabaseAttachment? { + val database: SQLiteDatabase = databaseHelper.readableDatabase + var cursor: Cursor? = null + + try { + cursor = database.query( + TABLE_NAME, + PROJECTION, + ROW_ID_WHERE, + arrayOf(attachmentId.rowId.toString()), + null, + null, + null + ) + + if (cursor != null && cursor.moveToFirst()) { + val list = getAttachment(cursor) + + if (list != null && list.size > 0) { + return list[0] + } + } + + return null + } finally { + cursor?.close() + } + } + + fun getAttachmentsForMessage(mmsId: Long): List { + val database: SQLiteDatabase = databaseHelper.readableDatabase + val results: MutableList = LinkedList() + var cursor: Cursor? = null + + try { + cursor = database.query( + TABLE_NAME, PROJECTION, MMS_ID + " = ?", arrayOf(mmsId.toString() + ""), + null, null, null + ) + + while (cursor != null && cursor.moveToNext()) { + val attachments = getAttachment(cursor) + for (attachment in attachments) { + if (attachment.isQuote) continue + results.add(attachment) + } + } + + return results + } finally { + cursor?.close() + } + } + + val pendingAttachments: List + get() { + val database: SQLiteDatabase = databaseHelper.readableDatabase + val attachments: MutableList = LinkedList() + + var cursor: Cursor? = null + try { + cursor = database.query( + TABLE_NAME, + PROJECTION, + TRANSFER_STATE + " = ?", + arrayOf(AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED.toString()), + null, + null, + null + ) + while (cursor != null && cursor.moveToNext()) { + attachments.addAll(getAttachment(cursor)) + } + } finally { + cursor?.close() + } + + return attachments + } + + fun deleteAttachmentsForMessages(messageIds: Array) { + val queryBuilder = StringBuilder() + for (i in messageIds.indices) { + queryBuilder.append(MMS_ID + " = ").append(messageIds[i]) + if (i + 1 < messageIds.size) { + queryBuilder.append(" OR ") + } + } + val idsAsString = queryBuilder.toString() + val database: SQLiteDatabase = databaseHelper.readableDatabase + var cursor: Cursor? = null + val attachmentInfos: MutableList = ArrayList() + try { + cursor = database.query( + TABLE_NAME, + arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), + idsAsString, + null, + null, + null, + null + ) + while (cursor != null && cursor.moveToNext()) { + attachmentInfos.add( + MmsAttachmentInfo( + cursor.getString(0), + cursor.getString(1), + cursor.getString(2) + ) + ) + } + } finally { + cursor?.close() + } + deleteAttachmentsOnDisk(attachmentInfos) + database.delete(TABLE_NAME, idsAsString, null) + notifyAttachmentListeners() + } + + fun deleteAttachmentsForMessage(mmsId: Long) { + val database: SQLiteDatabase = databaseHelper.writableDatabase + var cursor: Cursor? = null + + try { + cursor = database.query( + TABLE_NAME, arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), MMS_ID + " = ?", + arrayOf(mmsId.toString() + ""), null, null, null + ) + + while (cursor != null && cursor.moveToNext()) { + deleteAttachmentOnDisk( + cursor.getString(0), + cursor.getString(1), + cursor.getString(2) + ) + } + } finally { + cursor?.close() + } + + database.delete(TABLE_NAME, MMS_ID + " = ?", arrayOf(mmsId.toString() + "")) + notifyAttachmentListeners() + } + + fun deleteAttachmentsForMessages(mmsIds: LongArray?) { + val database: SQLiteDatabase = databaseHelper.writableDatabase + var cursor: Cursor? = null + val mmsIdString = StringUtils.join(mmsIds, ',') + + try { + cursor = database.query( + TABLE_NAME, arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), MMS_ID + " IN (?)", + arrayOf(mmsIdString), null, null, null + ) + + while (cursor != null && cursor.moveToNext()) { + deleteAttachmentOnDisk( + cursor.getString(0), + cursor.getString(1), + cursor.getString(2) + ) + } + } finally { + cursor?.close() + } + + database.delete(TABLE_NAME, MMS_ID + " IN (?)", arrayOf(mmsIdString)) + notifyAttachmentListeners() + } + + fun deleteAttachment(id: AttachmentId) { + val database: SQLiteDatabase = databaseHelper.writableDatabase + + database.query( + TABLE_NAME, + arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), + PART_ID_WHERE, + id.toStrings(), + null, + null, + null + ).use { cursor -> + if (cursor == null || !cursor.moveToNext()) { + Log.w(TAG, "Tried to delete an attachment, but it didn't exist.") + return + } + val data = cursor.getString(0) + val thumbnail = cursor.getString(1) + val contentType = cursor.getString(2) + + database.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings()) + deleteAttachmentOnDisk(data, thumbnail, contentType) + notifyAttachmentListeners() + } + } + + fun deleteAllAttachments() { + val database: SQLiteDatabase = databaseHelper.writableDatabase + database.delete(TABLE_NAME, null, null) + + val attachmentsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) + val attachments = attachmentsDirectory.listFiles() + + for (attachment in attachments) { + attachment.delete() + } + + notifyAttachmentListeners() + } + + private fun deleteAttachmentsOnDisk(mmsAttachmentInfos: List) { + for ((dataFile, thumbnailFile) in mmsAttachmentInfos) { + if (dataFile != null && !TextUtils.isEmpty(dataFile)) { + val data = File(dataFile) + if (data.exists()) { + data.delete() + } + } + if (thumbnailFile != null && !TextUtils.isEmpty(thumbnailFile)) { + val thumbnail = File(thumbnailFile) + if (thumbnail.exists()) { + thumbnail.delete() + } + } + } + + val anyImageType: Boolean = mmsAttachmentInfos.anyImages() + val anyThumbnail: Boolean = mmsAttachmentInfos.anyThumbnailNonNull() + + if (anyImageType || anyThumbnail) { + Glide.get(context).clearDiskCache() + } + } + + private fun deleteAttachmentOnDisk(data: String?, thumbnail: String?, contentType: String?) { + if (!TextUtils.isEmpty(data)) { + File(data).delete() + } + + if (!TextUtils.isEmpty(thumbnail)) { + File(thumbnail).delete() + } + + if (MediaUtil.isImageType(contentType) || thumbnail != null) { + Glide.get(context).clearDiskCache() + } + } + + @Throws(MmsException::class) + fun insertAttachmentsForPlaceholder( + mmsId: Long, + attachmentId: AttachmentId, + inputStream: InputStream + ) { + val placeholder = getAttachment(attachmentId) + val database: SQLiteDatabase = databaseHelper.writableDatabase + val values = ContentValues() + val dataInfo = setAttachmentData(inputStream) + + if (placeholder != null && placeholder.isQuote && !placeholder.contentType.startsWith("image")) { + values.put(THUMBNAIL, dataInfo.file.absolutePath) + values.put(THUMBNAIL_RANDOM, dataInfo.random) + } else { + values.put(DATA, dataInfo.file.absolutePath) + values.put(SIZE, dataInfo.length) + values.put(DATA_RANDOM, dataInfo.random) + } + + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) + values.put(CONTENT_LOCATION, null as String?) + values.put(CONTENT_DISPOSITION, null as String?) + values.put(DIGEST, null as ByteArray?) + values.put(NAME, null as String?) + values.put(FAST_PREFLIGHT_ID, null as String?) + values.put(URL, "") + + if (database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) { + dataInfo.file.delete() + } else { + notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(mmsId)) + notifyConversationListListeners() + } + + thumbnailExecutor.submit(ThumbnailFetchCallable(attachmentId)) + } + + fun updateAttachmentAfterUploadSucceeded(id: AttachmentId, attachment: Attachment) { + val database: SQLiteDatabase = databaseHelper.writableDatabase + val values = ContentValues() + + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) + values.put(CONTENT_LOCATION, attachment.location) + values.put(DIGEST, attachment.digest) + values.put(CONTENT_DISPOSITION, attachment.key) + values.put(NAME, attachment.relay) + values.put(SIZE, attachment.size) + values.put(FAST_PREFLIGHT_ID, attachment.fastPreflightId) + values.put(URL, attachment.url) + + database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) + } + + fun handleFailedAttachmentUpload(id: AttachmentId) { + val database: SQLiteDatabase = databaseHelper.writableDatabase + val values = ContentValues() + + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) + + database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) + } + + @Throws(MmsException::class) + fun insertAttachmentsForMessage( + mmsId: Long, + attachments: List, + quoteAttachment: List + ): Map { + Log.d(TAG, "insertParts(" + attachments.size + ")") + + val insertedAttachments: MutableMap = HashMap() + + for (attachment in attachments) { + val attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote) + insertedAttachments[attachment] = attachmentId + Log.i(TAG, "Inserted attachment at ID: $attachmentId") + } + + for (attachment in quoteAttachment) { + val attachmentId = insertAttachment(mmsId, attachment, true) + insertedAttachments[attachment] = attachmentId + Log.i(TAG, "Inserted quoted attachment at ID: $attachmentId") + } + + return insertedAttachments + } + + /** + * Insert attachments in database and return the IDs of the inserted attachments + * + * @param mmsId message ID + * @param attachments attachments to persist + * @return IDs of the persisted attachments + * @throws MmsException + */ + @Throws(MmsException::class) + fun insertAttachments(mmsId: Long, attachments: List): List { + Log.d(TAG, "insertParts(" + attachments.size + ")") + + val insertedAttachmentsIDs: MutableList = LinkedList() + + for (attachment in attachments) { + val attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote) + insertedAttachmentsIDs.add(attachmentId.rowId) + Log.i(TAG, "Inserted attachment at ID: $attachmentId") + } + + return insertedAttachmentsIDs + } + + @Throws(MmsException::class) + fun updateAttachmentData( + attachment: Attachment, + mediaStream: MediaStream + ): Attachment { + val database: SQLiteDatabase = databaseHelper.writableDatabase + val databaseAttachment = attachment as DatabaseAttachment + var dataInfo = getAttachmentDataFileInfo(databaseAttachment.attachmentId, DATA) + ?: throw MmsException("No attachment data found!") + + dataInfo = setAttachmentData(dataInfo.file, mediaStream.stream) + + val contentValues = ContentValues() + contentValues.put(SIZE, dataInfo.length) + contentValues.put(CONTENT_TYPE, mediaStream.mimeType) + contentValues.put(WIDTH, mediaStream.width) + contentValues.put(HEIGHT, mediaStream.height) + contentValues.put(DATA_RANDOM, dataInfo.random) + + database.update( + TABLE_NAME, + contentValues, + PART_ID_WHERE, + databaseAttachment.attachmentId.toStrings() + ) + + return DatabaseAttachment( + databaseAttachment.attachmentId, + databaseAttachment.mmsId, + databaseAttachment.hasData(), + databaseAttachment.hasThumbnail(), + mediaStream.mimeType, + databaseAttachment.transferState, + dataInfo.length, + databaseAttachment.fileName, + databaseAttachment.location, + databaseAttachment.key, + databaseAttachment.relay, + databaseAttachment.digest, + databaseAttachment.fastPreflightId, + databaseAttachment.isVoiceNote, + mediaStream.width, + mediaStream.height, + databaseAttachment.isQuote, + databaseAttachment.caption, + databaseAttachment.url + ) + } + + fun markAttachmentUploaded(messageId: Long, attachment: Attachment) { + val values = ContentValues(1) + val database: SQLiteDatabase = databaseHelper.writableDatabase + + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) + database.update( + TABLE_NAME, + values, + PART_ID_WHERE, + (attachment as DatabaseAttachment).attachmentId.toStrings() + ) + + notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(messageId)) + attachment.isUploaded = true + } + + fun setTransferState(messageId: Long, attachmentId: AttachmentId, transferState: Int) { + val values = ContentValues(1) + val database: SQLiteDatabase = databaseHelper.writableDatabase + + values.put(TRANSFER_STATE, transferState) + database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) + notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(messageId)) + } + + @VisibleForTesting + protected fun getDataStream( + attachmentId: AttachmentId, + dataType: String, + offset: Long + ): InputStream? { + val dataInfo = getAttachmentDataFileInfo(attachmentId, dataType) ?: return null + + try { + if (dataInfo.random != null && dataInfo.random.size == 32) { + return ModernDecryptingPartInputStream.createFor( + attachmentSecret, + dataInfo.random, + dataInfo.file, + offset + ) + } else { + val stream = ClassicDecryptingPartInputStream.createFor( + attachmentSecret, dataInfo.file + ) + val skipped = stream.skip(offset) + + if (skipped != offset) { + Log.w(TAG, "Skip failed: $skipped vs $offset") + return null + } + + return stream + } + } catch (e: IOException) { + Log.w(TAG, e) + return null + } + } + + private fun getAttachmentDataFileInfo(attachmentId: AttachmentId, dataType: String): DataInfo? { + val database: SQLiteDatabase = databaseHelper.readableDatabase + var cursor: Cursor? = null + + val randomColumn = when (dataType) { + DATA -> DATA_RANDOM + THUMBNAIL -> THUMBNAIL_RANDOM + else -> throw AssertionError("Unknown data type: $dataType") + } + try { + cursor = database.query( + TABLE_NAME, + arrayOf(dataType, SIZE, randomColumn), + PART_ID_WHERE, + attachmentId.toStrings(), + null, + null, + null + ) + + if (cursor != null && cursor.moveToFirst()) { + if (cursor.isNull(0)) { + return null + } + + return DataInfo( + File(cursor.getString(0)), + cursor.getLong(1), + cursor.getBlob(2) + ) + } else { + return null + } + } finally { + cursor?.close() + } + } + + @Throws(MmsException::class) + private fun setAttachmentData(uri: Uri): DataInfo { + try { + val inputStream = PartAuthority.getAttachmentStream(context, uri) + return setAttachmentData(inputStream) + } catch (e: IOException) { + throw MmsException(e) + } + } + + @Throws(MmsException::class) + private fun setAttachmentData(`in`: InputStream): DataInfo { + try { + val partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) + val dataFile = File.createTempFile("part", ".mms", partsDirectory) + return setAttachmentData(dataFile, `in`) + } catch (e: IOException) { + throw MmsException(e) + } + } + + @Throws(MmsException::class) + private fun setAttachmentData(destination: File, `in`: InputStream): DataInfo { + try { + val out = ModernEncryptingPartOutputStream.createFor( + attachmentSecret, destination, false + ) + val length = copy(`in`, out.second) + + return DataInfo(destination, length, out.first) + } catch (e: IOException) { + throw MmsException(e) + } + } + + fun getAttachment(cursor: Cursor): List { + try { + if (cursor.getColumnIndex(ATTACHMENT_JSON_ALIAS) != -1) { + if (cursor.isNull(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) { + return LinkedList() + } + + val result: MutableSet = + TreeSet { o1: DatabaseAttachment, o2: DatabaseAttachment -> if (o1.attachmentId == o2.attachmentId) 0 else 1 } + val array = JSONArray( + cursor.getString( + cursor.getColumnIndexOrThrow( + ATTACHMENT_JSON_ALIAS + ) + ) + ) + + for (i in 0 until array.length()) { + val `object` = SaneJSONObject(array.getJSONObject(i)) + + if (!`object`.isNull(ROW_ID)) { + result.add( + DatabaseAttachment( + AttachmentId( + `object`.getLong(ROW_ID), `object`.getLong( + UNIQUE_ID + ) + ), + `object`.getLong(MMS_ID), + !TextUtils.isEmpty(`object`.getString(DATA)), + !TextUtils.isEmpty(`object`.getString(THUMBNAIL)), + `object`.getString(CONTENT_TYPE), + `object`.getInt(TRANSFER_STATE), + `object`.getLong(SIZE), + `object`.getString(FILE_NAME), + `object`.getString(CONTENT_LOCATION), + `object`.getString(CONTENT_DISPOSITION), + `object`.getString(NAME), + null, + `object`.getString(FAST_PREFLIGHT_ID), + `object`.getInt(VOICE_NOTE) == 1, + `object`.getInt(WIDTH), + `object`.getInt(HEIGHT), + `object`.getInt(QUOTE) == 1, + `object`.getString(CAPTION), + "" + ) + ) // TODO: Not sure if this will break something + } + } + + return ArrayList(result) + } else { + val urlIndex = cursor.getColumnIndex(URL) + return listOf( + DatabaseAttachment( + AttachmentId( + cursor.getLong( + cursor.getColumnIndexOrThrow( + ROW_ID + ) + ), + cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID)) + ), + cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), + !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), + !cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), + cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), + cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), + cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), + cursor.getString(cursor.getColumnIndexOrThrow(NAME)), + cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)), + cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), + cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, + cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), + cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)), + cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1, + cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)), + if (urlIndex > 0) cursor.getString(urlIndex) else "" + ) + ) + } + } catch (e: JSONException) { + throw AssertionError(e) + } + } + + + @Throws(MmsException::class) + private fun insertAttachment( + mmsId: Long, + attachment: Attachment, + quote: Boolean + ): AttachmentId { + Log.d(TAG, "Inserting attachment for mms id: $mmsId") + + val database: SQLiteDatabase = databaseHelper.writableDatabase + var dataInfo: DataInfo? = null + val uniqueId = System.currentTimeMillis() + + if (attachment.dataUri != null) { + dataInfo = setAttachmentData(attachment.dataUri!!) + Log.d(TAG, "Wrote part to file: " + dataInfo.file.absolutePath) + } + + val contentValues = ContentValues() + contentValues.put(MMS_ID, mmsId) + contentValues.put(CONTENT_TYPE, attachment.contentType) + contentValues.put(TRANSFER_STATE, attachment.transferState) + contentValues.put(UNIQUE_ID, uniqueId) + contentValues.put(CONTENT_LOCATION, attachment.location) + contentValues.put(DIGEST, attachment.digest) + contentValues.put(CONTENT_DISPOSITION, attachment.key) + contentValues.put(NAME, attachment.relay) + contentValues.put(FILE_NAME, getCleanFileName(attachment.fileName)) + contentValues.put(SIZE, attachment.size) + contentValues.put(FAST_PREFLIGHT_ID, attachment.fastPreflightId) + contentValues.put(VOICE_NOTE, if (attachment.isVoiceNote) 1 else 0) + contentValues.put(WIDTH, attachment.width) + contentValues.put(HEIGHT, attachment.height) + contentValues.put(QUOTE, quote) + contentValues.put(CAPTION, attachment.caption) + contentValues.put(URL, attachment.url) + + if (dataInfo != null) { + contentValues.put(DATA, dataInfo.file.absolutePath) + contentValues.put(SIZE, dataInfo.length) + contentValues.put(DATA_RANDOM, dataInfo.random) + } + + val rowId = database.insert(TABLE_NAME, null, contentValues) + val attachmentId = AttachmentId(rowId, uniqueId) + val thumbnailUri = attachment.thumbnailUri + var hasThumbnail = false + + if (thumbnailUri != null) { + try { + PartAuthority.getAttachmentStream(context, thumbnailUri).use { attachmentStream -> + val dimens = if (attachment.contentType == MediaTypes.IMAGE_GIF) { + Pair(attachment.width, attachment.height) + } else { + BitmapUtil.getDimensions(attachmentStream) + } + updateAttachmentThumbnail( + attachmentId, + PartAuthority.getAttachmentStream(context, thumbnailUri), + dimens.first.toFloat() / dimens.second.toFloat() + ) + hasThumbnail = true + } + } catch (e: IOException) { + Log.w(TAG, "Failed to save existing thumbnail.", e) + } catch (e: BitmapDecodingException) { + Log.w(TAG, "Failed to save existing thumbnail.", e) + } + } + + if (!hasThumbnail && dataInfo != null) { + if (MediaUtil.hasVideoThumbnail(attachment.dataUri)) { + val bitmap = MediaUtil.getVideoThumbnail(context, attachment.dataUri) + + if (bitmap != null) { + val thumbnailData = ThumbnailData(bitmap) + updateAttachmentThumbnail( + attachmentId, + thumbnailData.toDataStream(), + thumbnailData.aspectRatio + ) + } else { + Log.w( + TAG, + "Retrieving video thumbnail failed, submitting thumbnail generation job..." + ) + thumbnailExecutor.submit(ThumbnailFetchCallable(attachmentId)) + } + } else { + Log.i(TAG, "Submitting thumbnail generation job...") + thumbnailExecutor.submit(ThumbnailFetchCallable(attachmentId)) + } + } + + return attachmentId + } + + @VisibleForTesting + @Throws(MmsException::class) + protected fun updateAttachmentThumbnail( + attachmentId: AttachmentId, + `in`: InputStream, + aspectRatio: Float + ) { + Log.i(TAG, "updating part thumbnail for #$attachmentId") + + val thumbnailFile = setAttachmentData(`in`) + + val database: SQLiteDatabase = databaseHelper.writableDatabase + val values = ContentValues(2) + + values.put(THUMBNAIL, thumbnailFile.file.absolutePath) + values.put(THUMBNAIL_ASPECT_RATIO, aspectRatio) + values.put(THUMBNAIL_RANDOM, thumbnailFile.random) + + database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) + + val cursor = database.query( + TABLE_NAME, + arrayOf(MMS_ID), + PART_ID_WHERE, + attachmentId.toStrings(), + null, + null, + null + ) + + try { + if (cursor != null && cursor.moveToFirst()) { + notifyConversationListeners( + get(context).mmsDatabase().getThreadIdForMessage( + cursor.getLong( + cursor.getColumnIndexOrThrow( + MMS_ID + ) + ) + ) + ) + } + } finally { + cursor?.close() + } + } + + /** + * Retrieves the audio extra values associated with the attachment. Only "audio/ *" mime type attachments are accepted. + * @return the related audio extras or null in case any of the audio extra columns are empty or the attachment is not an audio. + */ + @Synchronized + fun getAttachmentAudioExtras(attachmentId: AttachmentId): DatabaseAttachmentAudioExtras? { + databaseHelper.readableDatabase // We expect all the audio extra values to be present (not null) or reject the whole record. + .query( + TABLE_NAME, + PROJECTION_AUDIO_EXTRAS, + PART_ID_WHERE + + " AND " + AUDIO_VISUAL_SAMPLES + " IS NOT NULL" + + " AND " + AUDIO_DURATION + " IS NOT NULL" + + " AND " + PART_AUDIO_ONLY_WHERE, + attachmentId.toStrings(), + null, null, null, "1" + ).use { cursor -> + if (cursor == null || !cursor.moveToFirst()) return null + val audioSamples: ByteArray = cursor.getBlob( + cursor.getColumnIndexOrThrow( + AUDIO_VISUAL_SAMPLES + ) + ) + val duration: Long = cursor.getLong(cursor.getColumnIndexOrThrow(AUDIO_DURATION)) + return DatabaseAttachmentAudioExtras(attachmentId, audioSamples, duration) + } + } + + /** + * Updates audio extra columns for the "audio/ *" mime type attachments only. + * @return true if the update operation was successful. + */ + @Synchronized + fun setAttachmentAudioExtras(extras: DatabaseAttachmentAudioExtras, threadId: Long): Boolean { + val values = ContentValues() + values.put(AUDIO_VISUAL_SAMPLES, extras.visualSamples) + values.put(AUDIO_DURATION, extras.durationMs) + + val alteredRows: Int = databaseHelper.writableDatabase.update( + TABLE_NAME, + values, + PART_ID_WHERE + " AND " + PART_AUDIO_ONLY_WHERE, + extras.attachmentId.toStrings() + ) + + if (threadId >= 0) { + notifyConversationListeners(threadId) + } + + return alteredRows > 0 + } + + /** + * Updates audio extra columns for the "audio/ *" mime type attachments only. + * @return true if the update operation was successful. + */ + @Synchronized + fun setAttachmentAudioExtras(extras: DatabaseAttachmentAudioExtras): Boolean { + return setAttachmentAudioExtras(extras, -1) // -1 for no update + } + + @VisibleForTesting + internal inner class ThumbnailFetchCallable(private val attachmentId: AttachmentId) : + Callable { + @Throws(Exception::class) + override fun call(): InputStream? { + Log.d(TAG, "Executing thumbnail job...") + val stream = getDataStream(attachmentId, THUMBNAIL, 0) + + if (stream != null) { + return stream + } + + val attachment = getAttachment(attachmentId) + + if (attachment == null || !attachment.hasData()) { + return null + } + + var data: ThumbnailData? = null + + if (MediaUtil.isVideoType(attachment.contentType)) { + data = generateVideoThumbnail(attachmentId) + } + + if (data == null) { + return null + } + + updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.aspectRatio) + + return getDataStream(attachmentId, THUMBNAIL, 0) + } + + @SuppressLint("NewApi") + private fun generateVideoThumbnail(attachmentId: AttachmentId): ThumbnailData? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + Log.w(TAG, "Video thumbnails not supported...") + return null + } + + val dataInfo = getAttachmentDataFileInfo(attachmentId, DATA) + + if (dataInfo == null) { + Log.w(TAG, "No data file found for video thumbnail...") + return null + } + + val dataSource = EncryptedMediaDataSource( + attachmentSecret, + dataInfo.file, + dataInfo.random, + dataInfo.length + ) + val retriever = MediaMetadataRetriever() + retriever.setDataSource(dataSource) + + val bitmap = retriever.getFrameAtTime(1000) + + Log.i(TAG, "Generated video thumbnail...") + return ThumbnailData(bitmap) + } + } + + private class DataInfo(val file: File, val length: Long, random: ByteArray) { + val random: ByteArray? = random + } + + companion object { + private val TAG: String = AttachmentDatabase::class.java.simpleName + + const val TABLE_NAME: String = "part" + const val ROW_ID: String = "_id" + const val ATTACHMENT_JSON_ALIAS: String = "attachment_json" + const val MMS_ID: String = "mid" + const val CONTENT_TYPE: String = "ct" + const val NAME: String = "name" + const val CONTENT_DISPOSITION: String = "cd" + const val CONTENT_LOCATION: String = "cl" + const val DATA: String = "_data" + const val TRANSFER_STATE: String = "pending_push" + const val SIZE: String = "data_size" + const val FILE_NAME: String = "file_name" + const val THUMBNAIL: String = "thumbnail" + const val THUMBNAIL_ASPECT_RATIO: String = "aspect_ratio" + const val UNIQUE_ID: String = "unique_id" + const val DIGEST: String = "digest" + const val VOICE_NOTE: String = "voice_note" + const val QUOTE: String = "quote" + const val STICKER_PACK_ID: String = "sticker_pack_id" + const val STICKER_PACK_KEY: String = "sticker_pack_key" + const val STICKER_ID: String = "sticker_id" + const val FAST_PREFLIGHT_ID: String = "fast_preflight_id" + const val DATA_RANDOM: String = "data_random" + private const val THUMBNAIL_RANDOM = "thumbnail_random" + const val WIDTH: String = "width" + const val HEIGHT: String = "height" + const val CAPTION: String = "caption" + const val URL: String = "url" + const val DIRECTORY: String = "parts" + + // "audio/*" mime type only related columns. + const val AUDIO_VISUAL_SAMPLES: String = + "audio_visual_samples" // Small amount of audio byte samples to visualise the content (e.g. draw waveform). + const val AUDIO_DURATION: String = + "audio_duration" // Duration of the audio track in milliseconds. + + private const val PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?" + private const val ROW_ID_WHERE = ROW_ID + " = ?" + private const val PART_AUDIO_ONLY_WHERE = CONTENT_TYPE + " LIKE \"audio/%\"" + + private val PROJECTION = arrayOf( + ROW_ID, + MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, + CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE, + SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO, + UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, + QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT, + CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID, URL + ) + + private val PROJECTION_AUDIO_EXTRAS = arrayOf(AUDIO_VISUAL_SAMPLES, AUDIO_DURATION) + + const val CREATE_TABLE: String = + "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + + MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " + + CONTENT_TYPE + " TEXT, " + NAME + " TEXT, " + "chset" + " INTEGER, " + + CONTENT_DISPOSITION + " TEXT, " + "fn" + " TEXT, " + "cid" + " TEXT, " + + CONTENT_LOCATION + " TEXT, " + "ctt_s" + " INTEGER, " + + "ctt_t" + " TEXT, " + "encrypted" + " INTEGER, " + + TRANSFER_STATE + " INTEGER, " + DATA + " TEXT, " + SIZE + " INTEGER, " + + FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " + + UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " + + VOICE_NOTE + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + THUMBNAIL_RANDOM + " BLOB, " + + QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0, " + + CAPTION + " TEXT DEFAULT NULL, " + URL + " TEXT, " + STICKER_PACK_ID + " TEXT DEFAULT NULL, " + + STICKER_PACK_KEY + " DEFAULT NULL, " + STICKER_ID + " INTEGER DEFAULT -1," + + AUDIO_VISUAL_SAMPLES + " BLOB, " + AUDIO_DURATION + " INTEGER);" + + @JvmField + val CREATE_INDEXS: Array = arrayOf( + "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", + "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + TRANSFER_STATE + ");", + "CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON " + TABLE_NAME + " (" + STICKER_PACK_ID + ");", + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index 23a1af7cebe..599714e3cda 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -600,12 +600,11 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa if (!contentValues.containsKey(DATE_SENT)) { contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)) } - var quoteAttachments: List? = LinkedList() + val quoteAttachments: List = retrieved.quote.attachments ?: emptyList() if (retrieved.quote != null) { contentValues.put(QUOTE_ID, retrieved.quote.id) contentValues.put(QUOTE_AUTHOR, retrieved.quote.author.serialize()) contentValues.put(QUOTE_MISSING, if (retrieved.quote.missing) 1 else 0) - quoteAttachments = retrieved.quote.attachments } if (retrieved.isPushMessage && isDuplicate(retrieved, threadId) || retrieved.isMessageRequestResponse && isDuplicateMessageRequestResponse( @@ -619,7 +618,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val messageId = insertMediaMessage( retrieved.body, retrieved.attachments, - quoteAttachments!!, + quoteAttachments, retrieved.sharedContacts, retrieved.linkPreviews, contentValues, @@ -724,13 +723,12 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values).mapToLong { obj: Long -> obj } .sum()) - val quoteAttachments: MutableList = LinkedList() if (message.outgoingQuote != null) { contentValues.put(QUOTE_ID, message.outgoingQuote!!.id) contentValues.put(QUOTE_AUTHOR, message.outgoingQuote!!.author.serialize()) contentValues.put(QUOTE_MISSING, if (message.outgoingQuote!!.missing) 1 else 0) - quoteAttachments.addAll(message.outgoingQuote!!.attachments!!) } + val quoteAttachments = message.outgoingQuote?.attachments ?: emptyList() if (isDuplicate(message, threadId)) { Log.w(TAG, "Ignoring duplicate media message (" + message.sentTimeMillis + ")") return -1 @@ -781,8 +779,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa @Throws(MmsException::class) private fun insertMediaMessage( body: String?, - attachments: List, - quoteAttachments: List, + attachments: List, + quoteAttachments: List, sharedContacts: List, linkPreviews: List, contentValues: ContentValues, @@ -790,18 +788,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ): Long { val db = databaseHelper.writableDatabase val partsDatabase = get(context).attachmentDatabase() - val allAttachments: MutableList = LinkedList() - val contactAttachments = - Stream.of(sharedContacts).map { obj: Contact -> obj.avatarAttachment } - .filter { a: Attachment? -> a != null } - .toList() - val previewAttachments = - Stream.of(linkPreviews).filter { lp: LinkPreview -> lp.getThumbnail().isPresent } - .map { lp: LinkPreview -> lp.getThumbnail().get() } - .toList() - allAttachments.addAll(attachments) - allAttachments.addAll(contactAttachments) - allAttachments.addAll(previewAttachments) + val contactAttachments = sharedContacts.mapNotNull { it.avatarAttachment } + val previewAttachments = linkPreviews.mapNotNull { it.getThumbnail().orNull() } + val allAttachments = buildList { + addAll(attachments) + addAll(contactAttachments) + addAll(previewAttachments) + } contentValues.put(BODY, body) contentValues.put(PART_COUNT, allAttachments.size) db.beginTransaction() @@ -958,7 +951,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } private fun getSerializedSharedContacts( - insertedAttachmentIds: Map, + insertedAttachmentIds: Map, contacts: List ): String? { if (contacts.isEmpty()) return null @@ -989,7 +982,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } private fun getSerializedLinkPreviews( - insertedAttachmentIds: Map, + insertedAttachmentIds: Map, previews: List ): String? { if (previews.isEmpty()) return null From 5d088c99b1803e147306c58fea9966a334afd42e Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 12 Jul 2024 18:14:16 +0930 Subject: [PATCH 03/12] Cleanup AttachmentDatabase --- .../securesms/database/AttachmentDatabase.kt | 685 +++++------------- .../database/model/MmsAttachmentInfo.kt | 16 +- .../thoughtcrime/securesms/util/CursorUtil.kt | 4 +- 3 files changed, 194 insertions(+), 511 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt index b3d1e3500bb..eb33154912f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt @@ -22,13 +22,11 @@ import android.content.Context import android.database.Cursor import android.media.MediaMetadataRetriever import android.net.Uri -import android.os.Build import android.text.TextUtils import android.util.Pair import androidx.annotation.VisibleForTesting import com.bumptech.glide.Glide import net.zetetic.database.sqlcipher.SQLiteDatabase -import org.apache.commons.lang3.StringUtils import org.json.JSONArray import org.json.JSONException import org.session.libsession.messaging.sending_receiving.attachments.Attachment @@ -48,8 +46,8 @@ import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo -import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo.Companion.anyImages -import org.thoughtcrime.securesms.database.model.MmsAttachmentInfo.Companion.anyThumbnailNonNull +import org.thoughtcrime.securesms.database.model.isImage +import org.thoughtcrime.securesms.database.model.isThumbnailNonNull import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.mms.MediaStream import org.thoughtcrime.securesms.mms.MmsException @@ -58,48 +56,32 @@ import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData +import org.thoughtcrime.securesms.util.map import org.thoughtcrime.securesms.video.EncryptedMediaDataSource import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream -import java.util.LinkedList import java.util.TreeSet import java.util.concurrent.Callable import java.util.concurrent.ExecutionException class AttachmentDatabase( - context: Context?, - databaseHelper: SQLCipherOpenHelper?, + context: Context, + databaseHelper: SQLCipherOpenHelper, private val attachmentSecret: AttachmentSecret -) : Database( - context!!, databaseHelper!! -) { +) : Database(context, databaseHelper) { private val thumbnailExecutor = newSingleThreadedLifoExecutor() @Throws(IOException::class) - fun getAttachmentStream(attachmentId: AttachmentId, offset: Long): InputStream { - val dataStream = getDataStream(attachmentId, DATA, offset) - - if (dataStream == null) throw IOException("No stream for: $attachmentId") - else return dataStream - } + fun getAttachmentStream(attachmentId: AttachmentId, offset: Long): InputStream = + getDataStream(attachmentId, DATA, offset) ?: throw IOException("No stream for: $attachmentId") @Throws(IOException::class) fun getThumbnailStream(attachmentId: AttachmentId): InputStream { Log.d(TAG, "getThumbnailStream($attachmentId)") - val dataStream = getDataStream(attachmentId, THUMBNAIL, 0) - - if (dataStream != null) { - return dataStream - } - - try { - val generatedStream = - thumbnailExecutor.submit(ThumbnailFetchCallable(attachmentId)).get() - - if (generatedStream == null) throw FileNotFoundException("No thumbnail stream available: $attachmentId") - else return generatedStream + return getDataStream(attachmentId, THUMBNAIL, 0) ?: try { + thumbnailExecutor.submit(ThumbnailFetchCallable(attachmentId)).get() ?: throw FileNotFoundException("No thumbnail stream available: $attachmentId") } catch (ie: InterruptedException) { throw AssertionError("interrupted") } catch (ee: ExecutionException) { @@ -108,183 +90,59 @@ class AttachmentDatabase( } } - @Throws(MmsException::class) - fun setTransferProgressFailed(attachmentId: AttachmentId, mmsId: Long) { - val database: SQLiteDatabase = databaseHelper.writableDatabase - val values = ContentValues() - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) - - database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) - notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(mmsId)) - } - - fun getAttachment(attachmentId: AttachmentId): DatabaseAttachment? { - val database: SQLiteDatabase = databaseHelper.readableDatabase - var cursor: Cursor? = null - - try { - cursor = database.query( - TABLE_NAME, - PROJECTION, - ROW_ID_WHERE, - arrayOf(attachmentId.rowId.toString()), - null, - null, - null - ) - - if (cursor != null && cursor.moveToFirst()) { - val list = getAttachment(cursor) - - if (list != null && list.size > 0) { - return list[0] - } - } - - return null - } finally { - cursor?.close() - } - } - - fun getAttachmentsForMessage(mmsId: Long): List { - val database: SQLiteDatabase = databaseHelper.readableDatabase - val results: MutableList = LinkedList() - var cursor: Cursor? = null - - try { - cursor = database.query( - TABLE_NAME, PROJECTION, MMS_ID + " = ?", arrayOf(mmsId.toString() + ""), - null, null, null - ) - - while (cursor != null && cursor.moveToNext()) { - val attachments = getAttachment(cursor) - for (attachment in attachments) { - if (attachment.isQuote) continue - results.add(attachment) - } - } - - return results - } finally { - cursor?.close() - } - } - - val pendingAttachments: List - get() { - val database: SQLiteDatabase = databaseHelper.readableDatabase - val attachments: MutableList = LinkedList() - - var cursor: Cursor? = null - try { - cursor = database.query( - TABLE_NAME, - PROJECTION, - TRANSFER_STATE + " = ?", - arrayOf(AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED.toString()), - null, - null, - null - ) - while (cursor != null && cursor.moveToNext()) { - attachments.addAll(getAttachment(cursor)) - } - } finally { - cursor?.close() - } + fun getAttachment(attachmentId: AttachmentId): DatabaseAttachment? = databaseHelper.readableDatabase.query( + TABLE_NAME, + PROJECTION, + ROW_ID_WHERE, + arrayOf(attachmentId.rowId.toString()), + null, + null, + null + ).use { it.takeIf(Cursor::moveToFirst)?.let(::getAttachment)?.firstOrNull() } - return attachments - } + fun getAttachmentsForMessage(mmsId: Long) = databaseHelper.readableDatabase.query( + TABLE_NAME, PROJECTION, "$MMS_ID = ?", arrayOf(mmsId.toString()), + null, null, null + ).use { cursor -> cursor.map(::getAttachment).flatMap { it }.filterNot { it.isQuote }.toList() } fun deleteAttachmentsForMessages(messageIds: Array) { - val queryBuilder = StringBuilder() - for (i in messageIds.indices) { - queryBuilder.append(MMS_ID + " = ").append(messageIds[i]) - if (i + 1 < messageIds.size) { - queryBuilder.append(" OR ") - } - } - val idsAsString = queryBuilder.toString() + val idsAsString = messageIds.map { "$MMS_ID = $it" }.joinToString { " OR " } val database: SQLiteDatabase = databaseHelper.readableDatabase - var cursor: Cursor? = null - val attachmentInfos: MutableList = ArrayList() - try { - cursor = database.query( - TABLE_NAME, - arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), - idsAsString, - null, - null, - null, - null - ) - while (cursor != null && cursor.moveToNext()) { - attachmentInfos.add( - MmsAttachmentInfo( - cursor.getString(0), - cursor.getString(1), - cursor.getString(2) - ) + database.query( + TABLE_NAME, + arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), + idsAsString, + null, + null, + null, + null + ).use { cursor -> + cursor.map { + MmsAttachmentInfo( + it.getString(0), + it.getString(1), + it.getString(2) ) } - } finally { - cursor?.close() - } - deleteAttachmentsOnDisk(attachmentInfos) + }.let(::deleteAttachmentsOnDisk) + database.delete(TABLE_NAME, idsAsString, null) notifyAttachmentListeners() } fun deleteAttachmentsForMessage(mmsId: Long) { val database: SQLiteDatabase = databaseHelper.writableDatabase - var cursor: Cursor? = null - - try { - cursor = database.query( - TABLE_NAME, arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), MMS_ID + " = ?", - arrayOf(mmsId.toString() + ""), null, null, null - ) - - while (cursor != null && cursor.moveToNext()) { - deleteAttachmentOnDisk( - cursor.getString(0), - cursor.getString(1), - cursor.getString(2) - ) - } - } finally { - cursor?.close() - } - - database.delete(TABLE_NAME, MMS_ID + " = ?", arrayOf(mmsId.toString() + "")) - notifyAttachmentListeners() - } - - fun deleteAttachmentsForMessages(mmsIds: LongArray?) { - val database: SQLiteDatabase = databaseHelper.writableDatabase - var cursor: Cursor? = null - val mmsIdString = StringUtils.join(mmsIds, ',') - try { - cursor = database.query( - TABLE_NAME, arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), MMS_ID + " IN (?)", - arrayOf(mmsIdString), null, null, null - ) - - while (cursor != null && cursor.moveToNext()) { - deleteAttachmentOnDisk( - cursor.getString(0), - cursor.getString(1), - cursor.getString(2) - ) + database.query( + TABLE_NAME, arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), "$MMS_ID = ?", + arrayOf(mmsId.toString()), null, null, null + ).use { + while (it.moveToNext()) { + deleteAttachmentOnDisk(it.getString(0), it.getString(1), it.getString(2)) } - } finally { - cursor?.close() } - database.delete(TABLE_NAME, MMS_ID + " IN (?)", arrayOf(mmsIdString)) + database.delete(TABLE_NAME, "$MMS_ID = ?", arrayOf(mmsId.toString())) notifyAttachmentListeners() } @@ -300,7 +158,7 @@ class AttachmentDatabase( null, null ).use { cursor -> - if (cursor == null || !cursor.moveToNext()) { + if (!cursor.moveToNext()) { Log.w(TAG, "Tried to delete an attachment, but it didn't exist.") return } @@ -314,52 +172,22 @@ class AttachmentDatabase( } } - fun deleteAllAttachments() { - val database: SQLiteDatabase = databaseHelper.writableDatabase - database.delete(TABLE_NAME, null, null) - - val attachmentsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) - val attachments = attachmentsDirectory.listFiles() - - for (attachment in attachments) { - attachment.delete() - } - - notifyAttachmentListeners() - } - - private fun deleteAttachmentsOnDisk(mmsAttachmentInfos: List) { - for ((dataFile, thumbnailFile) in mmsAttachmentInfos) { - if (dataFile != null && !TextUtils.isEmpty(dataFile)) { - val data = File(dataFile) - if (data.exists()) { - data.delete() - } - } - if (thumbnailFile != null && !TextUtils.isEmpty(thumbnailFile)) { - val thumbnail = File(thumbnailFile) - if (thumbnail.exists()) { - thumbnail.delete() - } - } - } - - val anyImageType: Boolean = mmsAttachmentInfos.anyImages() - val anyThumbnail: Boolean = mmsAttachmentInfos.anyThumbnailNonNull() + private fun deleteAttachmentsOnDisk(mmsAttachmentInfos: Sequence) = mmsAttachmentInfos.run { + flatMap { sequenceOf(it.dataFile, it.thumbnailFile) } + .filterNotNull() + .filter(String::isNotEmpty) + .map(::File) + .filter(File::exists).toList() + .forEach(File::delete) - if (anyImageType || anyThumbnail) { + if (any { it.isImage() || it.isThumbnailNonNull() }) { Glide.get(context).clearDiskCache() } } private fun deleteAttachmentOnDisk(data: String?, thumbnail: String?, contentType: String?) { - if (!TextUtils.isEmpty(data)) { - File(data).delete() - } - - if (!TextUtils.isEmpty(thumbnail)) { - File(thumbnail).delete() - } + data?.takeUnless(String::isEmpty)?.let(::File)?.delete() + thumbnail?.takeUnless(String::isEmpty)?.let(::File)?.delete() if (MediaUtil.isImageType(contentType) || thumbnail != null) { Glide.get(context).clearDiskCache() @@ -373,11 +201,10 @@ class AttachmentDatabase( inputStream: InputStream ) { val placeholder = getAttachment(attachmentId) - val database: SQLiteDatabase = databaseHelper.writableDatabase val values = ContentValues() val dataInfo = setAttachmentData(inputStream) - if (placeholder != null && placeholder.isQuote && !placeholder.contentType.startsWith("image")) { + if (placeholder?.isQuote == true && !placeholder.contentType.startsWith("image")) { values.put(THUMBNAIL, dataInfo.file.absolutePath) values.put(THUMBNAIL_RANDOM, dataInfo.random) } else { @@ -394,7 +221,7 @@ class AttachmentDatabase( values.put(FAST_PREFLIGHT_ID, null as String?) values.put(URL, "") - if (database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) { + if (databaseHelper.writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) { dataInfo.file.delete() } else { notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(mmsId)) @@ -405,7 +232,6 @@ class AttachmentDatabase( } fun updateAttachmentAfterUploadSucceeded(id: AttachmentId, attachment: Attachment) { - val database: SQLiteDatabase = databaseHelper.writableDatabase val values = ContentValues() values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) @@ -417,16 +243,13 @@ class AttachmentDatabase( values.put(FAST_PREFLIGHT_ID, attachment.fastPreflightId) values.put(URL, attachment.url) - database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) + databaseHelper.writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) } fun handleFailedAttachmentUpload(id: AttachmentId) { - val database: SQLiteDatabase = databaseHelper.writableDatabase val values = ContentValues() - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) - - database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) + databaseHelper.writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) } @Throws(MmsException::class) @@ -437,21 +260,10 @@ class AttachmentDatabase( ): Map { Log.d(TAG, "insertParts(" + attachments.size + ")") - val insertedAttachments: MutableMap = HashMap() - - for (attachment in attachments) { - val attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote) - insertedAttachments[attachment] = attachmentId - Log.i(TAG, "Inserted attachment at ID: $attachmentId") - } - - for (attachment in quoteAttachment) { - val attachmentId = insertAttachment(mmsId, attachment, true) - insertedAttachments[attachment] = attachmentId - Log.i(TAG, "Inserted quoted attachment at ID: $attachmentId") - } - - return insertedAttachments + return attachments.associateWith { insertAttachment(mmsId, it, it.isQuote) } + .onEach { (_, id) -> Log.i(TAG, "Inserted attachment at ID: $id") } + + quoteAttachment.associateWith { insertAttachment(mmsId, it, true) } + .onEach { (_, id) -> Log.i(TAG, "Inserted quoted attachment at ID: $id") } } /** @@ -466,15 +278,11 @@ class AttachmentDatabase( fun insertAttachments(mmsId: Long, attachments: List): List { Log.d(TAG, "insertParts(" + attachments.size + ")") - val insertedAttachmentsIDs: MutableList = LinkedList() - - for (attachment in attachments) { - val attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote) - insertedAttachmentsIDs.add(attachmentId.rowId) - Log.i(TAG, "Inserted attachment at ID: $attachmentId") + return attachments.map { + insertAttachment(mmsId, it, it.isQuote) + .also { Log.i(TAG, "Inserted attachment at ID: $it") } + .rowId } - - return insertedAttachmentsIDs } @Throws(MmsException::class) @@ -482,12 +290,11 @@ class AttachmentDatabase( attachment: Attachment, mediaStream: MediaStream ): Attachment { - val database: SQLiteDatabase = databaseHelper.writableDatabase - val databaseAttachment = attachment as DatabaseAttachment - var dataInfo = getAttachmentDataFileInfo(databaseAttachment.attachmentId, DATA) - ?: throw MmsException("No attachment data found!") + attachment as DatabaseAttachment - dataInfo = setAttachmentData(dataInfo.file, mediaStream.stream) + val dataInfo = getAttachmentDataFileInfo(attachment.attachmentId, DATA) + .let { it ?: throw MmsException("No attachment data found!") } + .let { setAttachmentData(it.file, mediaStream.stream) } val contentValues = ContentValues() contentValues.put(SIZE, dataInfo.length) @@ -496,174 +303,123 @@ class AttachmentDatabase( contentValues.put(HEIGHT, mediaStream.height) contentValues.put(DATA_RANDOM, dataInfo.random) - database.update( + databaseHelper.writableDatabase.update( TABLE_NAME, contentValues, PART_ID_WHERE, - databaseAttachment.attachmentId.toStrings() + attachment.attachmentId.toStrings() ) return DatabaseAttachment( - databaseAttachment.attachmentId, - databaseAttachment.mmsId, - databaseAttachment.hasData(), - databaseAttachment.hasThumbnail(), + attachment.attachmentId, + attachment.mmsId, + attachment.hasData(), + attachment.hasThumbnail(), mediaStream.mimeType, - databaseAttachment.transferState, + attachment.transferState, dataInfo.length, - databaseAttachment.fileName, - databaseAttachment.location, - databaseAttachment.key, - databaseAttachment.relay, - databaseAttachment.digest, - databaseAttachment.fastPreflightId, - databaseAttachment.isVoiceNote, + attachment.fileName, + attachment.location, + attachment.key, + attachment.relay, + attachment.digest, + attachment.fastPreflightId, + attachment.isVoiceNote, mediaStream.width, mediaStream.height, - databaseAttachment.isQuote, - databaseAttachment.caption, - databaseAttachment.url - ) - } - - fun markAttachmentUploaded(messageId: Long, attachment: Attachment) { - val values = ContentValues(1) - val database: SQLiteDatabase = databaseHelper.writableDatabase - - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) - database.update( - TABLE_NAME, - values, - PART_ID_WHERE, - (attachment as DatabaseAttachment).attachmentId.toStrings() + attachment.isQuote, + attachment.caption, + attachment.url ) - - notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(messageId)) - attachment.isUploaded = true } fun setTransferState(messageId: Long, attachmentId: AttachmentId, transferState: Int) { val values = ContentValues(1) - val database: SQLiteDatabase = databaseHelper.writableDatabase values.put(TRANSFER_STATE, transferState) - database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) + databaseHelper.writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(messageId)) } @VisibleForTesting - protected fun getDataStream( + fun getDataStream( attachmentId: AttachmentId, dataType: String, offset: Long - ): InputStream? { - val dataInfo = getAttachmentDataFileInfo(attachmentId, dataType) ?: return null - + ): InputStream? = getAttachmentDataFileInfo(attachmentId, dataType)?.let { dataInfo -> try { - if (dataInfo.random != null && dataInfo.random.size == 32) { - return ModernDecryptingPartInputStream.createFor( - attachmentSecret, - dataInfo.random, - dataInfo.file, - offset - ) - } else { - val stream = ClassicDecryptingPartInputStream.createFor( - attachmentSecret, dataInfo.file - ) - val skipped = stream.skip(offset) - - if (skipped != offset) { - Log.w(TAG, "Skip failed: $skipped vs $offset") - return null - } - - return stream - } + if (dataInfo.random.size == 32) ModernDecryptingPartInputStream.createFor( + attachmentSecret, + dataInfo.random, + dataInfo.file, + offset + ) else ClassicDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.file) + .takeIf { it.skip(offset) == offset } } catch (e: IOException) { Log.w(TAG, e) - return null + null } } private fun getAttachmentDataFileInfo(attachmentId: AttachmentId, dataType: String): DataInfo? { - val database: SQLiteDatabase = databaseHelper.readableDatabase - var cursor: Cursor? = null - val randomColumn = when (dataType) { DATA -> DATA_RANDOM THUMBNAIL -> THUMBNAIL_RANDOM else -> throw AssertionError("Unknown data type: $dataType") } - try { - cursor = database.query( - TABLE_NAME, - arrayOf(dataType, SIZE, randomColumn), - PART_ID_WHERE, - attachmentId.toStrings(), - null, - null, - null - ) - - if (cursor != null && cursor.moveToFirst()) { - if (cursor.isNull(0)) { - return null - } - return DataInfo( - File(cursor.getString(0)), - cursor.getLong(1), - cursor.getBlob(2) + return databaseHelper.readableDatabase.query( + TABLE_NAME, + arrayOf(dataType, SIZE, randomColumn), + PART_ID_WHERE, + attachmentId.toStrings(), + null, + null, + null + ).use { cursor -> + cursor.takeIf { it.moveToFirst() && ! it.isNull(0) }?.run { + DataInfo( + File(getString(0)), + getLong(1), + getBlob(2) ) - } else { - return null } - } finally { - cursor?.close() } } @Throws(MmsException::class) - private fun setAttachmentData(uri: Uri): DataInfo { - try { - val inputStream = PartAuthority.getAttachmentStream(context, uri) - return setAttachmentData(inputStream) - } catch (e: IOException) { - throw MmsException(e) - } + private fun setAttachmentData(uri: Uri): DataInfo = try { + PartAuthority.getAttachmentStream(context, uri).let(::setAttachmentData) + } catch (e: IOException) { + throw MmsException(e) } @Throws(MmsException::class) - private fun setAttachmentData(`in`: InputStream): DataInfo { - try { - val partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) - val dataFile = File.createTempFile("part", ".mms", partsDirectory) - return setAttachmentData(dataFile, `in`) - } catch (e: IOException) { - throw MmsException(e) - } + private fun setAttachmentData(inputStream: InputStream): DataInfo = try { + val partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) + val dataFile = File.createTempFile("part", ".mms", partsDirectory) + setAttachmentData(dataFile, inputStream) + } catch (e: IOException) { + throw MmsException(e) } @Throws(MmsException::class) - private fun setAttachmentData(destination: File, `in`: InputStream): DataInfo { - try { - val out = ModernEncryptingPartOutputStream.createFor( - attachmentSecret, destination, false - ) - val length = copy(`in`, out.second) + private fun setAttachmentData(destination: File, inputStream: InputStream): DataInfo = try { + val out = ModernEncryptingPartOutputStream.createFor( + attachmentSecret, destination, false + ) + val length = copy(inputStream, out.second) - return DataInfo(destination, length, out.first) - } catch (e: IOException) { - throw MmsException(e) - } + DataInfo(destination, length, out.first) + } catch (e: IOException) { + throw MmsException(e) } fun getAttachment(cursor: Cursor): List { try { if (cursor.getColumnIndex(ATTACHMENT_JSON_ALIAS) != -1) { if (cursor.isNull(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) { - return LinkedList() + return emptyList() } val result: MutableSet = @@ -716,11 +472,7 @@ class AttachmentDatabase( return listOf( DatabaseAttachment( AttachmentId( - cursor.getLong( - cursor.getColumnIndexOrThrow( - ROW_ID - ) - ), + cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID)) ), cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), @@ -758,7 +510,6 @@ class AttachmentDatabase( ): AttachmentId { Log.d(TAG, "Inserting attachment for mms id: $mmsId") - val database: SQLiteDatabase = databaseHelper.writableDatabase var dataInfo: DataInfo? = null val uniqueId = System.currentTimeMillis() @@ -786,13 +537,13 @@ class AttachmentDatabase( contentValues.put(CAPTION, attachment.caption) contentValues.put(URL, attachment.url) - if (dataInfo != null) { - contentValues.put(DATA, dataInfo.file.absolutePath) - contentValues.put(SIZE, dataInfo.length) - contentValues.put(DATA_RANDOM, dataInfo.random) + dataInfo?.run { + contentValues.put(DATA, file.absolutePath) + contentValues.put(SIZE, length) + contentValues.put(DATA_RANDOM, random) } - val rowId = database.insert(TABLE_NAME, null, contentValues) + val rowId = databaseHelper.writableDatabase.insert(TABLE_NAME, null, contentValues) val attachmentId = AttachmentId(rowId, uniqueId) val thumbnailUri = attachment.thumbnailUri var hasThumbnail = false @@ -848,17 +599,17 @@ class AttachmentDatabase( @VisibleForTesting @Throws(MmsException::class) - protected fun updateAttachmentThumbnail( + private fun updateAttachmentThumbnail( attachmentId: AttachmentId, - `in`: InputStream, + inputStream: InputStream, aspectRatio: Float ) { Log.i(TAG, "updating part thumbnail for #$attachmentId") - val thumbnailFile = setAttachmentData(`in`) + val thumbnailFile = setAttachmentData(inputStream) val database: SQLiteDatabase = databaseHelper.writableDatabase - val values = ContentValues(2) + val values = ContentValues(3) values.put(THUMBNAIL, thumbnailFile.file.absolutePath) values.put(THUMBNAIL_ASPECT_RATIO, aspectRatio) @@ -866,7 +617,7 @@ class AttachmentDatabase( database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) - val cursor = database.query( + database.query( TABLE_NAME, arrayOf(MMS_ID), PART_ID_WHERE, @@ -874,22 +625,14 @@ class AttachmentDatabase( null, null, null - ) - - try { - if (cursor != null && cursor.moveToFirst()) { + ).use { cursor -> + cursor.takeIf { it.moveToFirst() }?.let { notifyConversationListeners( get(context).mmsDatabase().getThreadIdForMessage( - cursor.getLong( - cursor.getColumnIndexOrThrow( - MMS_ID - ) - ) + it.getLong(it.getColumnIndexOrThrow(MMS_ID)) ) ) } - } finally { - cursor?.close() } } @@ -898,28 +641,25 @@ class AttachmentDatabase( * @return the related audio extras or null in case any of the audio extra columns are empty or the attachment is not an audio. */ @Synchronized - fun getAttachmentAudioExtras(attachmentId: AttachmentId): DatabaseAttachmentAudioExtras? { + fun getAttachmentAudioExtras(attachmentId: AttachmentId): DatabaseAttachmentAudioExtras? = databaseHelper.readableDatabase // We expect all the audio extra values to be present (not null) or reject the whole record. .query( TABLE_NAME, PROJECTION_AUDIO_EXTRAS, - PART_ID_WHERE + - " AND " + AUDIO_VISUAL_SAMPLES + " IS NOT NULL" + - " AND " + AUDIO_DURATION + " IS NOT NULL" + - " AND " + PART_AUDIO_ONLY_WHERE, + "$PART_ID_WHERE AND $AUDIO_VISUAL_SAMPLES IS NOT NULL AND $AUDIO_DURATION IS NOT NULL AND $PART_AUDIO_ONLY_WHERE", attachmentId.toStrings(), null, null, null, "1" ).use { cursor -> - if (cursor == null || !cursor.moveToFirst()) return null - val audioSamples: ByteArray = cursor.getBlob( - cursor.getColumnIndexOrThrow( - AUDIO_VISUAL_SAMPLES - ) - ) - val duration: Long = cursor.getLong(cursor.getColumnIndexOrThrow(AUDIO_DURATION)) - return DatabaseAttachmentAudioExtras(attachmentId, audioSamples, duration) + cursor.takeIf { it.moveToFirst() } + ?.getBlob(cursor.getColumnIndexOrThrow(AUDIO_VISUAL_SAMPLES)) + ?.let { audioSamples -> + DatabaseAttachmentAudioExtras( + attachmentId, + audioSamples, + durationMs = cursor.getLong(cursor.getColumnIndexOrThrow(AUDIO_DURATION)) + ) + } } - } /** * Updates audio extra columns for the "audio/ *" mime type attachments only. @@ -934,7 +674,7 @@ class AttachmentDatabase( val alteredRows: Int = databaseHelper.writableDatabase.update( TABLE_NAME, values, - PART_ID_WHERE + " AND " + PART_AUDIO_ONLY_WHERE, + "$PART_ID_WHERE AND $PART_AUDIO_ONLY_WHERE", extras.attachmentId.toStrings() ) @@ -945,81 +685,33 @@ class AttachmentDatabase( return alteredRows > 0 } - /** - * Updates audio extra columns for the "audio/ *" mime type attachments only. - * @return true if the update operation was successful. - */ - @Synchronized - fun setAttachmentAudioExtras(extras: DatabaseAttachmentAudioExtras): Boolean { - return setAttachmentAudioExtras(extras, -1) // -1 for no update - } - @VisibleForTesting - internal inner class ThumbnailFetchCallable(private val attachmentId: AttachmentId) : - Callable { + internal inner class ThumbnailFetchCallable( + private val attachmentId: AttachmentId + ) : Callable { @Throws(Exception::class) override fun call(): InputStream? { Log.d(TAG, "Executing thumbnail job...") - val stream = getDataStream(attachmentId, THUMBNAIL, 0) - - if (stream != null) { - return stream - } - - val attachment = getAttachment(attachmentId) - - if (attachment == null || !attachment.hasData()) { - return null - } - - var data: ThumbnailData? = null - - if (MediaUtil.isVideoType(attachment.contentType)) { - data = generateVideoThumbnail(attachmentId) - } - - if (data == null) { - return null + return getDataStream(attachmentId, THUMBNAIL, 0) ?: attachmentId.takeIf { + getAttachment(it)?.hasVideoData == true + }?.let(::generateVideoThumbnail)?.let { + updateAttachmentThumbnail(attachmentId, it.toDataStream(), it.aspectRatio) + getDataStream(attachmentId, THUMBNAIL, 0) } - - updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.aspectRatio) - - return getDataStream(attachmentId, THUMBNAIL, 0) } @SuppressLint("NewApi") - private fun generateVideoThumbnail(attachmentId: AttachmentId): ThumbnailData? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - Log.w(TAG, "Video thumbnails not supported...") - return null - } - - val dataInfo = getAttachmentDataFileInfo(attachmentId, DATA) - - if (dataInfo == null) { - Log.w(TAG, "No data file found for video thumbnail...") - return null - } - - val dataSource = EncryptedMediaDataSource( - attachmentSecret, - dataInfo.file, - dataInfo.random, - dataInfo.length - ) - val retriever = MediaMetadataRetriever() - retriever.setDataSource(dataSource) - - val bitmap = retriever.getFrameAtTime(1000) - - Log.i(TAG, "Generated video thumbnail...") - return ThumbnailData(bitmap) - } + private fun generateVideoThumbnail(attachmentId: AttachmentId): ThumbnailData? = + getAttachmentDataFileInfo(attachmentId, DATA) + .also { it ?: Log.w(TAG, "No data file found for video thumbnail...") } + ?.run { EncryptedMediaDataSource(attachmentSecret, file, random, length) } + ?.let { MediaMetadataRetriever().apply { setDataSource(it) } } + ?.getFrameAtTime(1000) + ?.also { Log.i(TAG, "Generated video thumbnail...") } + ?.let(::ThumbnailData) } - private class DataInfo(val file: File, val length: Long, random: ByteArray) { - val random: ByteArray? = random - } + private class DataInfo(val file: File, val length: Long, val random: ByteArray) companion object { private val TAG: String = AttachmentDatabase::class.java.simpleName @@ -1055,14 +747,12 @@ class AttachmentDatabase( const val DIRECTORY: String = "parts" // "audio/*" mime type only related columns. - const val AUDIO_VISUAL_SAMPLES: String = - "audio_visual_samples" // Small amount of audio byte samples to visualise the content (e.g. draw waveform). - const val AUDIO_DURATION: String = - "audio_duration" // Duration of the audio track in milliseconds. + const val AUDIO_VISUAL_SAMPLES: String = "audio_visual_samples" // Small amount of audio byte samples to visualise the content (e.g. draw waveform). + const val AUDIO_DURATION: String = "audio_duration" // Duration of the audio track in milliseconds. - private const val PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?" - private const val ROW_ID_WHERE = ROW_ID + " = ?" - private const val PART_AUDIO_ONLY_WHERE = CONTENT_TYPE + " LIKE \"audio/%\"" + private const val PART_ID_WHERE = "$ROW_ID = ? AND $UNIQUE_ID = ?" + private const val ROW_ID_WHERE = "$ROW_ID = ?" + private const val PART_AUDIO_ONLY_WHERE = "$CONTENT_TYPE LIKE \"audio/%\"" private val PROJECTION = arrayOf( ROW_ID, @@ -1094,9 +784,12 @@ class AttachmentDatabase( @JvmField val CREATE_INDEXS: Array = arrayOf( - "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", - "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + TRANSFER_STATE + ");", - "CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON " + TABLE_NAME + " (" + STICKER_PACK_ID + ");", + "CREATE INDEX IF NOT EXISTS part_mms_id_index ON $TABLE_NAME ($MMS_ID);", + "CREATE INDEX IF NOT EXISTS pending_push_index ON $TABLE_NAME ($TRANSFER_STATE);", + "CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON $TABLE_NAME ($STICKER_PACK_ID);", ) } } + +private val DatabaseAttachment.hasVideoData: Boolean get() = + takeIf { it.hasData() }?.contentType?.let(MediaUtil::isVideoType) == true diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsAttachmentInfo.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsAttachmentInfo.kt index e1828a09014..2861b8dfa77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsAttachmentInfo.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsAttachmentInfo.kt @@ -2,16 +2,6 @@ package org.thoughtcrime.securesms.database.model import org.thoughtcrime.securesms.util.MediaUtil -data class MmsAttachmentInfo(val dataFile: String?, val thumbnailFile: String?, val contentType: String?) { - companion object { - @JvmStatic - fun List.anyImages() = any { - MediaUtil.isImageType(it.contentType) - } - - @JvmStatic - fun List.anyThumbnailNonNull() = any { - it.thumbnailFile?.isNotEmpty() == true - } - } -} \ No newline at end of file +data class MmsAttachmentInfo(val dataFile: String?, val thumbnailFile: String?, val contentType: String?) +fun MmsAttachmentInfo.isImage() = MediaUtil.isImageType(contentType) +fun MmsAttachmentInfo.isThumbnailNonNull() = thumbnailFile?.isNotEmpty() == true diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt index 9cff8c77bcf..5c576e23871 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt @@ -2,5 +2,5 @@ package org.thoughtcrime.securesms.util import android.database.Cursor -fun Cursor.asSequence(): Sequence = - generateSequence { if (moveToNext()) this else null } +fun Cursor.asSequence(): Sequence = generateSequence { takeIf { moveToNext() } } +fun Cursor.map(transform: (Cursor) -> T): Sequence = asSequence().map(transform) From 82a52ae11f172bf53245ab58dfa40a4051a7a412 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 12 Jul 2024 18:22:53 +0930 Subject: [PATCH 04/12] Convert DatabaseAttachment --- .../securesms/database/AttachmentDatabase.kt | 6 +- .../mediapreview/MediaPreviewViewModel.java | 4 +- .../securesms/util/AttachmentUtil.java | 4 +- .../messaging/jobs/AttachmentDownloadJob.kt | 2 +- .../attachments/DatabaseAttachment.java | 87 ------------------- .../attachments/DatabaseAttachment.kt | 35 ++++++++ .../link_preview/LinkPreview.java | 2 +- 7 files changed, 44 insertions(+), 96 deletions(-) delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.java create mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt index eb33154912f..e1d7b490451 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt @@ -313,8 +313,8 @@ class AttachmentDatabase( return DatabaseAttachment( attachment.attachmentId, attachment.mmsId, - attachment.hasData(), - attachment.hasThumbnail(), + attachment.hasData, + attachment.hasThumbnail, mediaStream.mimeType, attachment.transferState, dataInfo.length, @@ -792,4 +792,4 @@ class AttachmentDatabase( } private val DatabaseAttachment.hasVideoData: Boolean get() = - takeIf { it.hasData() }?.contentType?.let(MediaUtil::isVideoType) == true + takeIf { it.hasData }?.contentType?.let(MediaUtil::isVideoType) == true diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java index 91dad138483..68bc6d49408 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java @@ -57,7 +57,7 @@ public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) while (cursor.moveToPrevious()) { MediaRecord record = MediaRecord.from(context, cursor); - if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) { + if (record.getAttachment().mmsId == activeRecord.getAttachment().mmsId) { Media media = toMedia(record); if (media != null) rail.addFirst(media); } else { @@ -69,7 +69,7 @@ public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) while (cursor.moveToNext()) { MediaRecord record = MediaRecord.from(context, cursor); - if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) { + if (record.getAttachment().mmsId == activeRecord.getAttachment().mmsId) { Media media = toMedia(record); if (media != null) rail.addLast(media); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java index bda23c03776..d85a2438916 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java @@ -58,8 +58,8 @@ public static boolean isAutoDownloadPermitted(@NonNull Context context, @Nullabl public static void deleteAttachment(@NonNull Context context, @NonNull DatabaseAttachment attachment) { - AttachmentId attachmentId = attachment.getAttachmentId(); - long mmsId = attachment.getMmsId(); + AttachmentId attachmentId = attachment.attachmentId; + long mmsId = attachment.mmsId; int attachmentCount = DatabaseComponent.get(context).attachmentDatabase() .getAttachmentsForMessage(mmsId) .size(); diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index 0d057e81f13..5cc5ecb69ec 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -130,7 +130,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) try { val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) ?: return handleFailure(Error.NoAttachment, null) - if (attachment.hasData()) { + if (attachment.hasData) { handleFailure(Error.DuplicateData, attachment.attachmentId) return } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.java b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.java deleted file mode 100644 index 8d0831adf6e..00000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.session.libsession.messaging.sending_receiving.attachments; - -import android.net.Uri; - -import androidx.annotation.Nullable; - -import org.session.libsession.messaging.MessagingModuleConfiguration; - -public class DatabaseAttachment extends Attachment { - - private final AttachmentId attachmentId; - private final long mmsId; - private final boolean hasData; - private final boolean hasThumbnail; - private boolean isUploaded = false; - - public DatabaseAttachment(AttachmentId attachmentId, long mmsId, - boolean hasData, boolean hasThumbnail, - String contentType, int transferProgress, long size, - String fileName, String location, String key, String relay, - byte[] digest, String fastPreflightId, boolean voiceNote, - int width, int height, boolean quote, @Nullable String caption, - String url) - { - super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, caption, url); - this.attachmentId = attachmentId; - this.hasData = hasData; - this.hasThumbnail = hasThumbnail; - this.mmsId = mmsId; - } - - @Override - @Nullable - public Uri getDataUri() { - if (hasData) { - return MessagingModuleConfiguration.getShared().getStorage().getAttachmentDataUri(attachmentId); - } else { - return null; - } - } - - @Override - @Nullable - public Uri getThumbnailUri() { - if (hasThumbnail) { - return MessagingModuleConfiguration.getShared().getStorage().getAttachmentThumbnailUri(attachmentId); - } else { - return null; - } - } - - public AttachmentId getAttachmentId() { - return attachmentId; - } - - @Override - public boolean equals(Object other) { - return other != null && - other instanceof DatabaseAttachment && - ((DatabaseAttachment) other).attachmentId.equals(this.attachmentId); - } - - @Override - public int hashCode() { - return attachmentId.hashCode(); - } - - public long getMmsId() { - return mmsId; - } - - public boolean hasData() { - return hasData; - } - - public boolean hasThumbnail() { - return hasThumbnail; - } - - public boolean isUploaded() { - return isUploaded; - } - - public void setUploaded(boolean uploaded) { - isUploaded = uploaded; - } -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.kt new file mode 100644 index 00000000000..a2bd9015e03 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.kt @@ -0,0 +1,35 @@ +package org.session.libsession.messaging.sending_receiving.attachments + +import android.net.Uri +import org.session.libsession.messaging.MessagingModuleConfiguration.Companion.shared + +class DatabaseAttachment( + @JvmField val attachmentId: AttachmentId, @JvmField val mmsId: Long, + val hasData: Boolean, val hasThumbnail: Boolean, + contentType: String?, transferProgress: Int, size: Long, + fileName: String?, location: String?, key: String?, relay: String?, + digest: ByteArray?, fastPreflightId: String?, voiceNote: Boolean, + width: Int, height: Int, quote: Boolean, caption: String?, + url: String? +) : Attachment( + contentType!!, + transferProgress, + size, + fileName, + location, + key, + relay, + digest, + fastPreflightId, + voiceNote, + width, + height, + quote, + caption, + url +) { + override fun getDataUri(): Uri? = attachmentId.takeIf { hasData }?.let(shared.storage::getAttachmentDataUri) + override fun getThumbnailUri(): Uri? = attachmentId.takeIf { hasThumbnail }?.let(shared.storage::getAttachmentThumbnailUri) + override fun equals(other: Any?): Boolean = other != null && other is DatabaseAttachment && other.attachmentId == attachmentId + override fun hashCode(): Int = attachmentId.hashCode() +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java index b2b7cfc7d4d..ae5376b79d0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java @@ -33,7 +33,7 @@ public LinkPreview(@NonNull String url, @NonNull String title, @NonNull Database this.url = url; this.title = title; this.thumbnail = Optional.of(thumbnail); - this.attachmentId = thumbnail.getAttachmentId(); + this.attachmentId = thumbnail.attachmentId; } public LinkPreview(@NonNull String url, @NonNull String title, @NonNull Optional thumbnail) { From 0b953965681837a1a4f0588a63fc09d81c49d7be Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 12 Jul 2024 19:57:52 +0930 Subject: [PATCH 05/12] Convert MediaDatabase --- .../securesms/database/MediaDatabase.java | 149 ------------------ .../securesms/database/MediaDatabase.kt | 136 ++++++++++++++++ 2 files changed, 136 insertions(+), 149 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java deleted file mode 100644 index 63db0c66ba1..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ /dev/null @@ -1,149 +0,0 @@ -package org.thoughtcrime.securesms.database; - -import android.content.Context; -import android.database.ContentObserver; -import android.database.Cursor; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import net.zetetic.database.sqlcipher.SQLiteDatabase; - -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.utilities.Address; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; - -import java.util.List; - -public class MediaDatabase extends Database { - - private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DIGEST + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " - + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ADDRESS + " " - + "FROM " + AttachmentDatabase.TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME - + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " - + "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID - + " FROM " + MmsDatabase.TABLE_NAME - + " WHERE " + MmsDatabase.THREAD_ID + " = ?) AND (%s) AND " - + AttachmentDatabase.DATA + " IS NOT NULL AND " - + AttachmentDatabase.QUOTE + " = 0 AND " - + AttachmentDatabase.STICKER_PACK_ID + " IS NULL " - + "ORDER BY " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC"; - - private static final String GALLERY_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " LIKE 'image/%' OR " + AttachmentDatabase.CONTENT_TYPE + " LIKE 'video/%'"); - private static final String DOCUMENT_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'image/%' AND " + - AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'video/%' AND " + - AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'audio/%' AND " + - AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'text/x-signal-plain'"); - - public MediaDatabase(Context context, SQLCipherOpenHelper databaseHelper) { - super(context, databaseHelper); - } - - public Cursor getGalleryMediaForThread(long threadId) { - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""}); - setNotifyConversationListeners(cursor, threadId); - return cursor; - } - - public void subscribeToMediaChanges(@NonNull ContentObserver observer) { - registerAttachmentListeners(observer); - } - - public void unsubscribeToMediaChanges(@NonNull ContentObserver observer) { - context.getContentResolver().unregisterContentObserver(observer); - } - - public Cursor getDocumentMediaForThread(long threadId) { - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""}); - setNotifyConversationListeners(cursor, threadId); - return cursor; - } - - public static class MediaRecord { - - private final DatabaseAttachment attachment; - private final Address address; - private final long date; - private final boolean outgoing; - - private MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) { - this.attachment = attachment; - this.address = address; - this.date = date; - this.outgoing = outgoing; - } - - public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) { - AttachmentDatabase attachmentDatabase = DatabaseComponent.get(context).attachmentDatabase(); - List attachments = attachmentDatabase.getAttachment(cursor); - String serializedAddress = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)); - boolean outgoing = MessagingDatabase.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX))); - Address address = null; - - if (serializedAddress != null) { - address = Address.fromSerialized(serializedAddress); - } - - long date; - - if (MmsDatabase.Types.isPushType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)))) { - date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_SENT)); - } else { - date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED)); - } - - return new MediaRecord(attachments != null && attachments.size() > 0 ? attachments.get(0) : null, address, date, outgoing); - } - - public DatabaseAttachment getAttachment() { - return attachment; - } - - public String getContentType() { - return attachment.getContentType(); - } - - public @Nullable Address getAddress() { - return address; - } - - public long getDate() { - return date; - } - - public boolean isOutgoing() { - return outgoing; - } - - } - - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt new file mode 100644 index 00000000000..df6e9fba185 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt @@ -0,0 +1,136 @@ +package org.thoughtcrime.securesms.database + +import android.content.Context +import android.database.ContentObserver +import android.database.Cursor +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get + +class MediaDatabase(context: Context?, databaseHelper: SQLCipherOpenHelper?) : Database( + context!!, databaseHelper!! +) { + fun getGalleryMediaForThread(threadId: Long): Cursor { + val database: SQLiteDatabase = databaseHelper.getReadableDatabase() + val cursor = database.rawQuery(GALLERY_MEDIA_QUERY, arrayOf(threadId.toString() + "")) + setNotifyConversationListeners(cursor, threadId) + return cursor + } + + fun subscribeToMediaChanges(observer: ContentObserver) { + registerAttachmentListeners(observer) + } + + fun unsubscribeToMediaChanges(observer: ContentObserver) { + context.contentResolver.unregisterContentObserver(observer) + } + + fun getDocumentMediaForThread(threadId: Long): Cursor { + val database: SQLiteDatabase = databaseHelper.getReadableDatabase() + val cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, arrayOf(threadId.toString() + "")) + setNotifyConversationListeners(cursor, threadId) + return cursor + } + + class MediaRecord private constructor( + val attachment: DatabaseAttachment?, + val address: Address?, + val date: Long, + val isOutgoing: Boolean + ) { + val contentType: String + get() = attachment!!.contentType + + companion object { + fun from(context: Context, cursor: Cursor): MediaRecord { + val attachmentDatabase = get(context).attachmentDatabase() + val attachments = attachmentDatabase.getAttachment(cursor) + val serializedAddress = + cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)) + val outgoing: Boolean = MessagingDatabase.Types.isOutgoingMessageType( + cursor.getLong( + cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX) + ) + ) + var address: Address? = null + + if (serializedAddress != null) { + address = fromSerialized(serializedAddress) + } + + val date = if (MmsDatabase.Types.isPushType( + cursor.getLong( + cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX) + ) + ) + ) { + cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_SENT)) + } else { + cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED)) + } + + return MediaRecord( + if (attachments != null && attachments.size > 0) attachments[0] else null, + address, + date, + outgoing + ) + } + } + } + + + companion object { + private val BASE_MEDIA_QUERY = + ("SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DIGEST + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ADDRESS + " " + + "FROM " + AttachmentDatabase.TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME + + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + + "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID + + " FROM " + MmsDatabase.TABLE_NAME + + " WHERE " + MmsDatabase.THREAD_ID + " = ?) AND (%s) AND " + + AttachmentDatabase.DATA + " IS NOT NULL AND " + + AttachmentDatabase.QUOTE + " = 0 AND " + + AttachmentDatabase.STICKER_PACK_ID + " IS NULL " + + "ORDER BY " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC") + + private val GALLERY_MEDIA_QUERY = String.format( + BASE_MEDIA_QUERY, + AttachmentDatabase.CONTENT_TYPE + " LIKE 'image/%' OR " + AttachmentDatabase.CONTENT_TYPE + " LIKE 'video/%'" + ) + private val DOCUMENT_MEDIA_QUERY = String.format( + BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'image/%' AND " + + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'video/%' AND " + + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'audio/%' AND " + + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'text/x-signal-plain'" + ) + } +} From 697ba6247c4b695b60fee56f6c6156734e6a963a Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 12 Jul 2024 19:58:13 +0930 Subject: [PATCH 06/12] Cleanup MediaDatabase --- .../securesms/MediaDocumentsAdapter.java | 8 +- .../securesms/MediaGalleryAdapter.java | 2 +- .../securesms/MediaOverviewActivity.java | 18 +- .../securesms/MediaPreviewActivity.java | 14 +- .../securesms/database/GroupMemberDatabase.kt | 4 +- .../securesms/database/JsonUtil.kt | 22 + .../securesms/database/MediaDatabase.kt | 63 +-- .../securesms/database/MessagingDatabase.java | 1 - .../securesms/database/MmsDatabase.kt | 519 +++++++----------- .../loaders/BucketedThreadMediaLoader.java | 4 +- .../mediapreview/MediaPreviewViewModel.java | 20 +- .../thoughtcrime/securesms/util/CursorUtil.kt | 2 + 12 files changed, 291 insertions(+), 386 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/JsonUtil.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaDocumentsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/MediaDocumentsAdapter.java index bbc00472a59..49479f250ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaDocumentsAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaDocumentsAdapter.java @@ -56,11 +56,11 @@ public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { @Override public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) { MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor); - Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment()); + Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.attachment); if (slide != null && slide.hasDocument()) { viewHolder.documentView.setDocument((DocumentSlide)slide, false); - viewHolder.date.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.getDate())); + viewHolder.date.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.date)); viewHolder.documentView.setVisibility(View.VISIBLE); viewHolder.date.setVisibility(View.VISIBLE); viewHolder.documentView.setOnClickListener(view -> { @@ -91,7 +91,7 @@ public long getHeaderId(int position) { Cursor cursor = getCursorAtPositionOrThrow(position); MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor); - calendar.setTime(new Date(mediaRecord.getDate())); + calendar.setTime(new Date(mediaRecord.date)); return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR)); } @@ -104,7 +104,7 @@ public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { Cursor cursor = getCursorAtPositionOrThrow(position); MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor); - viewHolder.textView.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.getDate())); + viewHolder.textView.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.date)); } public static class ViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java index 62766d1cd78..b5f055d0350 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaGalleryAdapter.java @@ -111,7 +111,7 @@ public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int off MediaRecord mediaRecord = media.get(section, offset); ThumbnailView thumbnailView = ((ViewHolder)viewHolder).imageView; View selectedIndicator = ((ViewHolder)viewHolder).selectedIndicator; - Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment()); + Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.attachment); if (slide != null) { thumbnailView.setImageResource(glideRequests, slide, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java index 95ba15c82e8..929497799fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaOverviewActivity.java @@ -286,7 +286,7 @@ private void handleMediaMultiSelectClick(@NonNull MediaDatabase.MediaRecord medi } private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRecord) { - if (mediaRecord.getAttachment().getDataUri() == null) { + if (mediaRecord.attachment.getDataUri() == null) { return; } @@ -296,13 +296,13 @@ private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRec } Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate()); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize()); + intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.date); + intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.attachment.getSize()); intent.putExtra(MediaPreviewActivity.ADDRESS_EXTRA, recipient.getAddress()); intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing()); intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true); - intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType()); + intent.setDataAndType(mediaRecord.attachment.getDataUri(), mediaRecord.getContentType()); context.startActivity(intent); } @@ -337,11 +337,11 @@ protected List doInBackground(Void... params) { List attachments = new LinkedList<>(); for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) { - if (mediaRecord.getAttachment().getDataUri() != null) { - attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(), + if (mediaRecord.attachment.getDataUri() != null) { + attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.attachment.getDataUri(), mediaRecord.getContentType(), - mediaRecord.getDate(), - mediaRecord.getAttachment().getFileName())); + mediaRecord.date, + mediaRecord.attachment.getFileName())); } } @@ -391,7 +391,7 @@ protected Void doInBackground(MediaDatabase.MediaRecord... records) { } for (MediaDatabase.MediaRecord record : records) { - AttachmentUtil.deleteAttachment(getContext(), record.getAttachment()); + AttachmentUtil.deleteAttachment(getContext(), record.attachment); } return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 2e67becbfd1..2e596866c8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -707,8 +707,8 @@ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { try { //noinspection ConstantConditions - mediaView.set(glideRequests, window, mediaRecord.getAttachment().getDataUri(), - mediaRecord.getAttachment().getContentType(), mediaRecord.getAttachment().getSize(), autoplay); + mediaView.set(glideRequests, window, mediaRecord.attachment.getDataUri(), + mediaRecord.attachment.getContentType(), mediaRecord.attachment.getSize(), autoplay); } catch (IOException e) { Log.w(TAG, e); } @@ -731,15 +731,15 @@ public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Obj public MediaItem getMediaItemFor(int position) { cursor.moveToPosition(getCursorPosition(position)); MediaRecord mediaRecord = MediaRecord.from(context, cursor); - Address address = mediaRecord.getAddress(); + Address address = mediaRecord.address; - if (mediaRecord.getAttachment().getDataUri() == null) throw new AssertionError(); + if (mediaRecord.attachment.getDataUri() == null) throw new AssertionError(); return new MediaItem(address != null ? Recipient.from(context, address,true) : null, - mediaRecord.getAttachment(), - mediaRecord.getAttachment().getDataUri(), + mediaRecord.attachment, + mediaRecord.attachment.getDataUri(), mediaRecord.getContentType(), - mediaRecord.getDate(), + mediaRecord.date, mediaRecord.isOutgoing()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupMemberDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupMemberDatabase.kt index e869f741c77..28c8791317a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupMemberDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupMemberDatabase.kt @@ -8,6 +8,7 @@ import org.session.libsession.messaging.open_groups.GroupMember import org.session.libsession.messaging.open_groups.GroupMemberRole import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.util.asSequence +import org.thoughtcrime.securesms.util.map import java.util.EnumSet class GroupMemberDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { @@ -61,8 +62,7 @@ class GroupMemberDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab """.trimIndent() return readableDatabase.rawQuery(sql, groupId, JSONArray(memberIDs).toString()).use { cursor -> - cursor.asSequence() - .map { readGroupMember(it) } + cursor.map { readGroupMember(it) } .groupBy(keySelector = { it.profileId }, valueTransform = { it.role }) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/JsonUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/JsonUtil.kt new file mode 100644 index 00000000000..181a9f61950 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/JsonUtil.kt @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.database + +import org.json.JSONArray +import org.json.JSONObject + +fun JSONArray.forEach(action: (JSONObject) -> Unit) { + for (i in 0 until length()) { + action(getJSONObject(i)) + } +} + +fun JSONArray.map(transform: (JSONObject) -> T): List = buildList { + for (i in 0 until length()) { + add(transform(getJSONObject(i))) + } +} + +fun JSONArray.toList(): List = buildList { + for (i in 0 until length()) { + add(getJSONObject(i)) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt index df6e9fba185..69bc5b5d133 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt @@ -7,6 +7,8 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.thoughtcrime.securesms.database.MmsSmsColumns.ADDRESS +import org.thoughtcrime.securesms.database.MmsSmsColumns.THREAD_ID import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get @@ -14,7 +16,7 @@ class MediaDatabase(context: Context?, databaseHelper: SQLCipherOpenHelper?) : D context!!, databaseHelper!! ) { fun getGalleryMediaForThread(threadId: Long): Cursor { - val database: SQLiteDatabase = databaseHelper.getReadableDatabase() + val database: SQLiteDatabase = databaseHelper.readableDatabase val cursor = database.rawQuery(GALLERY_MEDIA_QUERY, arrayOf(threadId.toString() + "")) setNotifyConversationListeners(cursor, threadId) return cursor @@ -28,52 +30,39 @@ class MediaDatabase(context: Context?, databaseHelper: SQLCipherOpenHelper?) : D context.contentResolver.unregisterContentObserver(observer) } - fun getDocumentMediaForThread(threadId: Long): Cursor { - val database: SQLiteDatabase = databaseHelper.getReadableDatabase() - val cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, arrayOf(threadId.toString() + "")) - setNotifyConversationListeners(cursor, threadId) - return cursor - } + fun getDocumentMediaForThread(threadId: Long): Cursor = + databaseHelper.readableDatabase.rawQuery(DOCUMENT_MEDIA_QUERY, arrayOf(threadId.toString() + "")) + .also { setNotifyConversationListeners(it, threadId) } class MediaRecord private constructor( - val attachment: DatabaseAttachment?, - val address: Address?, - val date: Long, - val isOutgoing: Boolean + @JvmField val attachment: DatabaseAttachment?, + @JvmField val address: Address?, + @JvmField val date: Long, + val isOutgoing: Boolean ) { val contentType: String get() = attachment!!.contentType companion object { + @JvmStatic fun from(context: Context, cursor: Cursor): MediaRecord { val attachmentDatabase = get(context).attachmentDatabase() val attachments = attachmentDatabase.getAttachment(cursor) - val serializedAddress = - cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)) - val outgoing: Boolean = MessagingDatabase.Types.isOutgoingMessageType( - cursor.getLong( - cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX) - ) - ) - var address: Address? = null - - if (serializedAddress != null) { - address = fromSerialized(serializedAddress) - } + val serializedAddress = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.ADDRESS)) + val outgoing: Boolean = cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX) + .let(cursor::getLong) + .let(MmsSmsColumns.Types::isOutgoingMessageType) + val address: Address? = serializedAddress?.let(::fromSerialized) - val date = if (MmsDatabase.Types.isPushType( - cursor.getLong( - cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX) - ) - ) - ) { - cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_SENT)) - } else { - cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED)) - } + val date = when { + cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX) + .let(cursor::getLong) + .let(MmsSmsColumns.Types::isPushType) -> MmsDatabase.DATE_SENT + else -> MmsDatabase.DATE_RECEIVED + }.let(cursor::getColumnIndexOrThrow).let(cursor::getLong) return MediaRecord( - if (attachments != null && attachments.size > 0) attachments[0] else null, + attachments.firstOrNull(), address, date, outgoing @@ -111,12 +100,12 @@ class MediaDatabase(context: Context?, databaseHelper: SQLCipherOpenHelper?) : D + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ADDRESS + " " + + MmsDatabase.TABLE_NAME + "." + MmsSmsColumns.ADDRESS + " " + "FROM " + AttachmentDatabase.TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME - + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsSmsColumns.ID + " " + "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID + " FROM " + MmsDatabase.TABLE_NAME - + " WHERE " + MmsDatabase.THREAD_ID + " = ?) AND (%s) AND " + + " WHERE " + THREAD_ID + " = ?) AND (%s) AND " + AttachmentDatabase.DATA + " IS NOT NULL AND " + AttachmentDatabase.QUOTE + " = 0 AND " + AttachmentDatabase.STICKER_PACK_ID + " IS NULL " diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java index bc74496dda4..f6773dfffd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -14,7 +14,6 @@ import org.session.libsignal.crypto.IdentityKey; import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.util.SqlUtil; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index 599714e3cda..297e6163eb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import android.database.Cursor +import androidx.sqlite.db.transaction import com.annimon.stream.Stream import com.google.android.mms.pdu_alt.PduHeaders import org.apache.commons.lang3.StringUtils @@ -63,7 +64,8 @@ import org.thoughtcrime.securesms.database.model.Quote import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.mms.MmsException import org.thoughtcrime.securesms.mms.SlideDeck -import org.thoughtcrime.securesms.util.asSequence +import org.thoughtcrime.securesms.util.filter +import org.thoughtcrime.securesms.util.map import java.io.Closeable import java.io.IOException import java.security.SecureRandom @@ -74,35 +76,28 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa private val earlyReadReceiptCache = EarlyReceiptCache() override fun getTableName() = TABLE_NAME - fun getMessageCountForThread(threadId: Long): Int { - val db = databaseHelper.readableDatabase - db.query( - TABLE_NAME, - arrayOf("COUNT(*)"), - "$THREAD_ID = ?", - arrayOf(threadId.toString()), - null, - null, - null - ).use { cursor -> - if (cursor.moveToFirst()) return cursor.getInt(0) - } - return 0 - } + fun getMessageCountForThread(threadId: Long): Int = databaseHelper.readableDatabase.query( + TABLE_NAME, + arrayOf("COUNT(*)"), + "$THREAD_ID = ?", + arrayOf(threadId.toString()), + null, + null, + null + ).use { it.takeIf(Cursor::moveToFirst)?.getInt(0) } ?: 0 fun isOutgoingMessage(timestamp: Long): Boolean = databaseHelper.writableDatabase.query( TABLE_NAME, arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS), - DATE_SENT + " = ?", + "$DATE_SENT = ?", arrayOf(timestamp.toString()), null, null, null, null ).use { cursor -> - cursor.asSequence() - .map { cursor.getColumnIndexOrThrow(MESSAGE_BOX) } + cursor.map { it.getColumnIndexOrThrow(MESSAGE_BOX) } .map(cursor::getLong) .any { MmsSmsColumns.Types.isOutgoingMessageType(it) } } @@ -114,19 +109,17 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa readReceipt: Boolean ) { val database = databaseHelper.writableDatabase - var cursor: Cursor? = null var found = false - try { - cursor = database.query( - TABLE_NAME, - arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS), - "$DATE_SENT = ?", - arrayOf(messageId.timetamp.toString()), - null, - null, - null, - null - ) + database.query( + TABLE_NAME, + arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS), + "$DATE_SENT = ?", + arrayOf(messageId.timetamp.toString()), + null, + null, + null, + null + ).use { cursor -> while (cursor.moveToNext()) { if (MmsSmsColumns.Types.isOutgoingMessageType( cursor.getLong( @@ -144,44 +137,31 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ) ) val ourAddress = messageId.address - val columnName = - if (deliveryReceipt) DELIVERY_RECEIPT_COUNT else READ_RECEIPT_COUNT if (ourAddress.equals(theirAddress) || theirAddress.isGroup) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)) - val status = - if (deliveryReceipt) GroupReceiptDatabase.STATUS_DELIVERED else GroupReceiptDatabase.STATUS_READ + val status = if (deliveryReceipt) GroupReceiptDatabase.STATUS_DELIVERED else GroupReceiptDatabase.STATUS_READ + val columnName = if (deliveryReceipt) DELIVERY_RECEIPT_COUNT else READ_RECEIPT_COUNT found = true database.execSQL( - "UPDATE " + TABLE_NAME + " SET " + - columnName + " = " + columnName + " + 1 WHERE " + ID + " = ?", + "UPDATE $TABLE_NAME SET $columnName = $columnName + 1 WHERE $ID = ?", arrayOf(id.toString()) ) - get(context).groupReceiptDatabase() - .update(ourAddress, id, status, timestamp) + get(context).groupReceiptDatabase().update(ourAddress, id, status, timestamp) get(context).threadDatabase().update(threadId, false, true) notifyConversationListeners(threadId) } } } - if (!found) { - if (deliveryReceipt) earlyDeliveryReceiptCache.increment( - messageId.timetamp, - messageId.address - ) - if (readReceipt) earlyReadReceiptCache.increment( - messageId.timetamp, - messageId.address - ) + if (!found) messageId.run { + if (deliveryReceipt) earlyDeliveryReceiptCache.increment(timetamp, address) + if (readReceipt) earlyReadReceiptCache.increment(timetamp, address) } - } finally { - cursor?.close() } } fun updateSentTimestamp(messageId: Long, newTimestamp: Long, threadId: Long) { - val db = databaseHelper.writableDatabase - db.execSQL( + databaseHelper.writableDatabase.execSQL( "UPDATE $TABLE_NAME SET $DATE_SENT = ? WHERE $ID = ?", arrayOf(newTimestamp.toString(), messageId.toString()) ) @@ -189,28 +169,19 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa notifyConversationListListeners() } - fun getThreadIdForMessage(id: Long): Long { - val sql = "SELECT $THREAD_ID FROM $TABLE_NAME WHERE $ID = ?" - val sqlArgs = arrayOf(id.toString()) - val db = databaseHelper.readableDatabase - var cursor: Cursor? = null - return try { - cursor = db.rawQuery(sql, sqlArgs) - if (cursor != null && cursor.moveToFirst()) cursor.getLong(0) else -1 - } finally { - cursor?.close() - } + fun getThreadIdForMessage(id: Long): Long = databaseHelper.readableDatabase.rawQuery( + "SELECT $THREAD_ID FROM $TABLE_NAME WHERE $ID = ?", + arrayOf(id.toString()) + ).use { + it.takeIf { it.moveToFirst() }?.getLong(0) ?: -1 } - private fun rawQuery(where: String, arguments: Array?): Cursor { - val database = databaseHelper.readableDatabase - return database.rawQuery( - "SELECT " + MMS_PROJECTION.joinToString(",") + " FROM " + TABLE_NAME + - " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + " ON (" + TABLE_NAME + "." + ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" + - " LEFT OUTER JOIN " + ReactionDatabase.TABLE_NAME + " ON (" + TABLE_NAME + "." + ID + " = " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + " AND " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + " = 1)" + - " WHERE " + where + " GROUP BY " + TABLE_NAME + "." + ID, arguments - ) - } + private fun rawQuery(where: String, arguments: Array? = null): Cursor = databaseHelper.readableDatabase.rawQuery( + "SELECT " + MMS_PROJECTION.joinToString(",") + " FROM " + TABLE_NAME + + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + " ON (" + TABLE_NAME + "." + ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" + + " LEFT OUTER JOIN " + ReactionDatabase.TABLE_NAME + " ON (" + TABLE_NAME + "." + ID + " = " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + " AND " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + " = 1)" + + " WHERE " + where + " GROUP BY " + TABLE_NAME + "." + ID, arguments + ) fun getMessage(messageId: Long): Cursor { val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString())) @@ -218,32 +189,21 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa return cursor } - fun getRecentChatMemberIDs(threadID: Long, limit: Int): List { - val sql = """ - SELECT DISTINCT $ADDRESS FROM $TABLE_NAME - WHERE $THREAD_ID = ? - ORDER BY $DATE_SENT DESC - LIMIT $limit - """.trimIndent() - - return databaseHelper.readableDatabase.rawQuery(sql, threadID).use { cursor -> - cursor.asSequence() - .map { it.getString(0) } - .toList() - } + fun getRecentChatMemberIDs(threadID: Long, limit: Int): List = databaseHelper.readableDatabase.rawQuery( + """ + SELECT DISTINCT $ADDRESS FROM $TABLE_NAME + WHERE $THREAD_ID = ? + ORDER BY $DATE_SENT DESC + LIMIT $limit + """.trimIndent(), + threadID + ).use { cursor -> + cursor.map { it.getString(0) }.toList() } - val expireStartedMessages: Reader - get() { - val where = "$EXPIRE_STARTED > 0" - return readerFor(rawQuery(where, null))!! - } + val expireStartedMessages: Reader get() = readerFor(rawQuery(where = "$EXPIRE_STARTED > 0")) - val expireNotStartedMessages: Reader - get() { - val where = "$EXPIRES_IN > 0 AND $EXPIRE_STARTED = 0" - return readerFor(rawQuery(where, null))!! - } + val expireNotStartedMessages: Reader get() = readerFor(rawQuery(where = "$EXPIRES_IN > 0 AND $EXPIRE_STARTED = 0", null)) private fun updateMailboxBitmask( id: Long, @@ -251,14 +211,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa maskOn: Long, threadId: Optional ) { - val db = databaseHelper.writableDatabase - db.execSQL( + databaseHelper.writableDatabase.execSQL( "UPDATE " + TABLE_NAME + " SET " + MESSAGE_BOX + " = (" + MESSAGE_BOX + " & " + (MmsSmsColumns.Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" + " WHERE " + ID + " = ?", arrayOf(id.toString() + "") ) - if (threadId.isPresent) { - get(context).threadDatabase().update(threadId.get(), false, true) + threadId.orNull()?.let { + get(context).threadDatabase().update(it, false, true) } } @@ -337,230 +296,174 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa fun setMessagesRead(threadId: Long, beforeTime: Long): List { return setMessagesRead( - THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?", + "$THREAD_ID = ? AND ($READ = 0 OR $REACTIONS_UNREAD = 1) AND $DATE_SENT <= ?", arrayOf(threadId.toString(), beforeTime.toString()) ) } fun setMessagesRead(threadId: Long): List { return setMessagesRead( - THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", + "$THREAD_ID = ? AND ($READ = 0 OR $REACTIONS_UNREAD = 1)", arrayOf(threadId.toString()) ) } - private fun setMessagesRead(where: String, arguments: Array?): List { - val database = databaseHelper.writableDatabase - val result: MutableList = LinkedList() - var cursor: Cursor? = null - database.beginTransaction() - try { - cursor = database.query( - TABLE_NAME, - arrayOf(ID, ADDRESS, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED), - where, - arguments, - null, - null, - null - ) - while (cursor != null && cursor.moveToNext()) { - if (MmsSmsColumns.Types.isSecureType(cursor.getLong(3))) { - val timestamp = cursor.getLong(2) - val syncMessageId = SyncMessageId(fromSerialized(cursor.getString(1)), timestamp) - val expirationInfo = ExpirationInfo( - id = cursor.getLong(0), - timestamp = timestamp, - expiresIn = cursor.getLong(4), - expireStarted = cursor.getLong(5), - isMms = true + private fun setMessagesRead(where: String, arguments: Array?): List = + databaseHelper.writableDatabase.let { database -> + database.transaction { + database.query( + TABLE_NAME, + arrayOf(ID, ADDRESS, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED), + where, + arguments, + null, + null, + null + ).use { cursor -> + cursor.filter { MmsSmsColumns.Types.isSecureType(it.getLong(3)) }.map { + val timestamp = cursor.getLong(2) + MarkedMessageInfo( + syncMessageId = SyncMessageId(fromSerialized(cursor.getString(1)), timestamp), + expirationInfo = ExpirationInfo( + id = cursor.getLong(0), + timestamp = timestamp, + expiresIn = cursor.getLong(4), + expireStarted = cursor.getLong(5), + isMms = true + ) + ) + }.toList() + }.also { + database.update( + TABLE_NAME, + ContentValues().apply { + put(READ, 1) + put(REACTIONS_UNREAD, 0) + }, + where, + arguments ) - result.add(MarkedMessageInfo(syncMessageId, expirationInfo)) } } - val contentValues = ContentValues() - contentValues.put(READ, 1) - contentValues.put(REACTIONS_UNREAD, 0) - database.update(TABLE_NAME, contentValues, where, arguments) - database.setTransactionSuccessful() - } finally { - cursor?.close() - database.endTransaction() } - return result - } @Throws(MmsException::class, NoSuchMessageException::class) fun getOutgoingMessage(messageId: Long): OutgoingMediaMessage { val attachmentDatabase = get(context).attachmentDatabase() - var cursor: Cursor? = null - try { - cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString())) - if (cursor.moveToNext()) { - val associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId) - val outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)) - val body = cursor.getString(cursor.getColumnIndexOrThrow(BODY)) - val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)) - val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID)) - val expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN)) - val expireStartedAt = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED)) - val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)) - val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)) - val distributionType = get(context).threadDatabase().getDistributionType(threadId) - val mismatchDocument = cursor.getString( - cursor.getColumnIndexOrThrow( - MISMATCHED_IDENTITIES - ) - ) - val networkDocument = cursor.getString( - cursor.getColumnIndexOrThrow( - NETWORK_FAILURE - ) + rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString())).use { cursor -> + if (!cursor.moveToNext()) throw NoSuchMessageException("No record found for id: $messageId") + val associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId) + val outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)) + val body = cursor.getString(cursor.getColumnIndexOrThrow(BODY)) + val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)) + val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID)) + val expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN)) + val expireStartedAt = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED)) + val address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)) + val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)) + val distributionType = get(context).threadDatabase().getDistributionType(threadId) + val mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MISMATCHED_IDENTITIES)) + val networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(NETWORK_FAILURE)) + val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)) + val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)) + val quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY)) // TODO: this should be the referenced quote + val quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1 + val quoteAttachments = associatedAttachments.filter { it.isQuote } + val contacts = getSharedContacts(cursor, associatedAttachments) + val contactAttachments: Set = contacts.mapNotNull { it.avatarAttachment }.toSet() + val previews = getLinkPreviews(cursor, associatedAttachments) + val previewAttachments = previews.mapNotNull { it.getThumbnail().orNull() } + val attachments = associatedAttachments.filterNot { it.isQuote || it in contactAttachments || it in previewAttachments } + val recipient = Recipient.from(context, fromSerialized(address), false) + val quote: QuoteModel? = if (quoteId > 0 && (!quoteText.isNullOrEmpty() || quoteAttachments.isNotEmpty())) { + QuoteModel( + quoteId, + fromSerialized(quoteAuthor), + quoteText, // TODO: refactor this to use referenced quote + quoteMissing, + quoteAttachments ) - val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)) - val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)) - val quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY)) // TODO: this should be the referenced quote - val quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1 - val quoteAttachments = associatedAttachments - .filter { obj: DatabaseAttachment -> obj.isQuote } - val contacts = getSharedContacts(cursor, associatedAttachments) - val contactAttachments: Set = - contacts.mapNotNull { obj: Contact -> obj.avatarAttachment }.toSet() - val previews = getLinkPreviews(cursor, associatedAttachments) - val previewAttachments = - previews.filter { lp: LinkPreview -> lp.getThumbnail().isPresent } - .map { lp: LinkPreview -> lp.getThumbnail().get() } - val attachments = associatedAttachments - .asSequence() - .filterNot { obj: DatabaseAttachment -> obj.isQuote || contactAttachments.contains(obj) || previewAttachments.contains(obj) } - .toList() - val recipient = Recipient.from(context, fromSerialized(address), false) - var networkFailures: List? = LinkedList() - var mismatches: List? = LinkedList() - var quote: QuoteModel? = null - if (quoteId > 0 && (!quoteText.isNullOrEmpty() || quoteAttachments.isNotEmpty())) { - quote = QuoteModel( - quoteId, - fromSerialized(quoteAuthor), - quoteText, // TODO: refactor this to use referenced quote - quoteMissing, - quoteAttachments - ) - } - if (!mismatchDocument.isNullOrEmpty()) { - try { - mismatches = JsonUtil.fromJson( - mismatchDocument, - IdentityKeyMismatchList::class.java - ).list - } catch (e: IOException) { - Log.w(TAG, e) - } + } else null + val mismatches = mismatchDocument.takeUnless{ it.isNullOrEmpty() }?.let { + try { + JsonUtil.fromJson(it, IdentityKeyMismatchList::class.java).list + } catch (e: IOException) { + Log.w(TAG, e) + emptyList() } - if (!networkDocument.isNullOrEmpty()) { - try { - networkFailures = - JsonUtil.fromJson(networkDocument, NetworkFailureList::class.java).list - } catch (e: IOException) { - Log.w(TAG, e) - } + } ?: emptyList() + val networkFailures = networkDocument?.takeUnless { it.isEmpty() }?.let { + try { + JsonUtil.fromJson(it, NetworkFailureList::class.java).list + } catch (e: IOException) { + Log.w(TAG, e) + emptyList() } - val message = OutgoingMediaMessage( - recipient, - body, - attachments, - timestamp, - subscriptionId, - expiresIn, - expireStartedAt, - distributionType, - quote, - contacts, - previews, - networkFailures!!, - mismatches!! - ) - return if (MmsSmsColumns.Types.isSecureType(outboxType)) { - OutgoingSecureMediaMessage(message) - } else message - } - throw NoSuchMessageException("No record found for id: $messageId") - } finally { - cursor?.close() + } ?: emptyList() + val message = OutgoingMediaMessage( + recipient, + body, + attachments, + timestamp, + subscriptionId, + expiresIn, + expireStartedAt, + distributionType, + quote, + contacts, + previews, + networkFailures, + mismatches + ) + return if (MmsSmsColumns.Types.isSecureType(outboxType)) { + OutgoingSecureMediaMessage(message) + } else message } } private fun getSharedContacts( cursor: Cursor, attachments: List - ): List { - val serializedContacts = cursor.getString(cursor.getColumnIndexOrThrow(SHARED_CONTACTS)) - if (serializedContacts.isNullOrEmpty()) { - return emptyList() - } - val attachmentIdMap: MutableMap = HashMap() - for (attachment in attachments) { - attachmentIdMap[attachment.attachmentId] = attachment - } - try { - val contacts: MutableList = LinkedList() - val jsonContacts = JSONArray(serializedContacts) - for (i in 0 until jsonContacts.length()) { - val contact = Contact.deserialize(jsonContacts.getJSONObject(i).toString()) - if (contact.avatar != null && contact.avatar!!.attachmentId != null) { - val attachment = attachmentIdMap[contact.avatar!!.attachmentId] - val updatedAvatar = Contact.Avatar( - contact.avatar!!.attachmentId, - attachment, - contact.avatar!!.isProfile - ) - contacts.add(Contact(contact, updatedAvatar)) - } else { - contacts.add(contact) - } - } - return contacts - } catch (e: JSONException) { - Log.w(TAG, "Failed to parse shared contacts.", e) - } catch (e: IOException) { - Log.w(TAG, "Failed to parse shared contacts.", e) - } - return emptyList() + ): List = try { + val attachmentIdMap = attachments.associateBy { it.attachmentId } + cursor.getString(cursor.getColumnIndexOrThrow(SHARED_CONTACTS)) + ?.takeUnless { it.isEmpty() } + ?.let(::JSONArray) + ?.map { Contact.deserialize(it.toString()) }?.map { contact -> + contact.avatar?.takeIf { it.attachmentId != null }?.run { + Contact.Avatar(attachmentId, attachmentIdMap[attachmentId], isProfile) + .let { Contact(contact, it) } + } ?: contact + } ?: emptyList() + } catch (e: JSONException) { + Log.w(TAG, "Failed to parse shared contacts.", e) + emptyList() + } catch (e: IOException) { + Log.w(TAG, "Failed to parse shared contacts.", e) + emptyList() } private fun getLinkPreviews( cursor: Cursor, attachments: List - ): List { - val serializedPreviews = cursor.getString(cursor.getColumnIndexOrThrow(LINK_PREVIEWS)) - if (serializedPreviews.isNullOrEmpty()) { - return emptyList() - } - val attachmentIdMap: MutableMap = HashMap() - for (attachment in attachments) { - attachmentIdMap[attachment.attachmentId] = attachment - } - try { - val previews: MutableList = LinkedList() - val jsonPreviews = JSONArray(serializedPreviews) - for (i in 0 until jsonPreviews.length()) { - val preview = LinkPreview.deserialize(jsonPreviews.getJSONObject(i).toString()) - if (preview.attachmentId != null) { - val attachment = attachmentIdMap[preview.attachmentId] - if (attachment != null) { - previews.add(LinkPreview(preview.url, preview.title, attachment)) - } - } else { - previews.add(preview) - } - } - return previews - } catch (e: JSONException) { - Log.w(TAG, "Failed to parse shared contacts.", e) - } catch (e: IOException) { - Log.w(TAG, "Failed to parse shared contacts.", e) - } - return emptyList() + ): List = try { + val attachmentIdMap = attachments.associateBy { it.attachmentId } + cursor.getString(cursor.getColumnIndexOrThrow(LINK_PREVIEWS)) + ?.takeUnless { it.isEmpty() } + ?.let(::JSONArray) + ?.map{ it.toString() } + ?.map(LinkPreview::deserialize) + ?.map { preview -> + preview.attachmentId?.let { id -> + attachmentIdMap[id]?.let { LinkPreview(preview.url, preview.title, it) } + } ?: preview + } ?: emptyList() + } catch (e: JSONException) { + Log.w(TAG, "Failed to parse shared contacts.", e) + emptyList() + } catch (e: IOException) { + Log.w(TAG, "Failed to parse shared contacts.", e) + emptyList() } @Throws(MmsException::class) @@ -582,10 +485,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa contentValues.put(CONTENT_LOCATION, contentLocation) contentValues.put(STATUS, Status.DOWNLOAD_INITIALIZED) // In open groups messages should be sorted by their server timestamp - var receivedTimestamp = serverTimestamp - if (serverTimestamp == 0L) { - receivedTimestamp = retrieved.sentTimeMillis - } + val receivedTimestamp = serverTimestamp.takeUnless { it == 0L } ?: retrieved.sentTimeMillis contentValues.put( DATE_RECEIVED, receivedTimestamp @@ -601,10 +501,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)) } val quoteAttachments: List = retrieved.quote.attachments ?: emptyList() - if (retrieved.quote != null) { - contentValues.put(QUOTE_ID, retrieved.quote.id) - contentValues.put(QUOTE_AUTHOR, retrieved.quote.author.serialize()) - contentValues.put(QUOTE_MISSING, if (retrieved.quote.missing) 1 else 0) + retrieved.quote?.run { + contentValues.put(QUOTE_ID, id) + contentValues.put(QUOTE_AUTHOR, author.serialize()) + contentValues.put(QUOTE_MISSING, if (missing) 1 else 0) } if (retrieved.isPushMessage && isDuplicate(retrieved, threadId) || retrieved.isMessageRequestResponse && isDuplicateMessageRequestResponse( @@ -624,10 +524,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa contentValues, null, ) - if (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) { - if (runThreadUpdate) { - get(context).threadDatabase().update(threadId, true, true) - } + if (runThreadUpdate && !MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) { + get(context).threadDatabase().update(threadId, true, true) } notifyConversationListeners(threadId) return Optional.of(InsertResult(messageId, threadId)) @@ -1223,7 +1121,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa inner class Reader(private val cursor: Cursor?, private val getQuote: Boolean = true) : Closeable { val next: MessageRecord? - get() = if (cursor == null || !cursor.moveToNext()) null else current + get() = cursor?.takeIf { it.moveToNext() }?.let { current } val current: MessageRecord get() { val mmsType = cursor!!.getLong(cursor.getColumnIndexOrThrow(MESSAGE_TYPE)) @@ -1308,19 +1206,14 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val recipient = getRecipientFor(address) val mismatches = getMismatchedIdentities(mismatchDocument) val networkFailures = getFailures(networkDocument) - val attachments = get(context).attachmentDatabase().getAttachment( - cursor - ) - val contacts: List = getSharedContacts(cursor, attachments) - val contactAttachments: Set = - contacts.mapNotNull { it?.avatarAttachment }.toSet() + val attachments = get(context).attachmentDatabase().getAttachment(cursor) + val contacts: List = getSharedContacts(cursor, attachments) + val contactAttachments: Set = contacts.mapNotNull { it.avatarAttachment }.toSet() val previews: List = getLinkPreviews(cursor, attachments) val previewAttachments: Set = previews.mapNotNull { it?.getThumbnail()?.orNull() }.toSet() val slideDeck = getSlideDeck( - attachments - .filterNot { o: DatabaseAttachment? -> o in contactAttachments } - .filterNot { o: DatabaseAttachment? -> o in previewAttachments } + attachments.filterNot { it in contactAttachments || it in previewAttachments } ) val quote = if (getQuote) getQuote(cursor) else null val reactions = get(context).reactionDatabase().getReactions(cursor) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java index 05c13cc55da..e9743f98d14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java @@ -100,7 +100,7 @@ public BucketedThreadMedia(@NonNull Context context) { public void add(MediaDatabase.MediaRecord mediaRecord) { for (TimeBucket timeSection : TIME_SECTIONS) { - if (timeSection.inRange(mediaRecord.getDate())) { + if (timeSection.inRange(mediaRecord.date)) { timeSection.add(mediaRecord); return; } @@ -188,7 +188,7 @@ private static class MonthBuckets { void add(MediaDatabase.MediaRecord record) { Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(record.getDate()); + calendar.setTimeInMillis(record.date); int year = calendar.get(Calendar.YEAR) - 1900; int month = calendar.get(Calendar.MONTH); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java index 68bc6d49408..ed0ffb28abf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java @@ -57,7 +57,7 @@ public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) while (cursor.moveToPrevious()) { MediaRecord record = MediaRecord.from(context, cursor); - if (record.getAttachment().mmsId == activeRecord.getAttachment().mmsId) { + if (record.attachment.mmsId == activeRecord.attachment.mmsId) { Media media = toMedia(record); if (media != null) rail.addFirst(media); } else { @@ -69,7 +69,7 @@ public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) while (cursor.moveToNext()) { MediaRecord record = MediaRecord.from(context, cursor); - if (record.getAttachment().mmsId == activeRecord.getAttachment().mmsId) { + if (record.attachment.mmsId == activeRecord.attachment.mmsId) { Media media = toMedia(record); if (media != null) rail.addLast(media); } else { @@ -82,7 +82,7 @@ public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) } previewData.postValue(new PreviewData(rail.size() > 1 ? rail : Collections.emptyList(), - activeRecord.getAttachment().getCaption(), + activeRecord.attachment.getCaption(), rail.indexOf(activeMedia))); } @@ -96,8 +96,8 @@ private int getCursorPosition(int position) { } private @Nullable Media toMedia(@NonNull MediaRecord mediaRecord) { - Uri uri = mediaRecord.getAttachment().getThumbnailUri() != null ? mediaRecord.getAttachment().getThumbnailUri() - : mediaRecord.getAttachment().getDataUri(); + Uri uri = mediaRecord.attachment.getThumbnailUri() != null ? mediaRecord.attachment.getThumbnailUri() + : mediaRecord.attachment.getDataUri(); if (uri == null) { return null; @@ -105,12 +105,12 @@ private int getCursorPosition(int position) { return new Media(uri, mediaRecord.getContentType(), - mediaRecord.getDate(), - mediaRecord.getAttachment().getWidth(), - mediaRecord.getAttachment().getHeight(), - mediaRecord.getAttachment().getSize(), + mediaRecord.date, + mediaRecord.attachment.getWidth(), + mediaRecord.attachment.getHeight(), + mediaRecord.attachment.getSize(), Optional.absent(), - Optional.fromNullable(mediaRecord.getAttachment().getCaption())); + Optional.fromNullable(mediaRecord.attachment.getCaption())); } public LiveData getPreviewData() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt index 5c576e23871..56e117a5813 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.kt @@ -3,4 +3,6 @@ package org.thoughtcrime.securesms.util import android.database.Cursor fun Cursor.asSequence(): Sequence = generateSequence { takeIf { moveToNext() } } +fun Cursor.filter(predicate: (Cursor) -> Boolean): Sequence = asSequence().filter(predicate) fun Cursor.map(transform: (Cursor) -> T): Sequence = asSequence().map(transform) +fun Cursor.mapNotNull(transform: (Cursor) -> T): Sequence = asSequence().mapNotNull(transform) From 8eb917e4819b73c3ed1cf21f7e15174ac36a2c51 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 18 Jul 2024 12:24:51 +0930 Subject: [PATCH 07/12] Cleanup --- .../securesms/database/AttachmentDatabase.kt | 56 ++++++++----------- .../securesms/database/JsonUtil.kt | 18 +----- .../securesms/database/MediaDatabase.kt | 4 +- .../securesms/database/MmsDatabase.kt | 18 +++--- .../securesms/home/HomeActivity.kt | 9 ++- 5 files changed, 43 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt index e1d7b490451..1cf1a164410 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt @@ -377,7 +377,7 @@ class AttachmentDatabase( null, null ).use { cursor -> - cursor.takeIf { it.moveToFirst() && ! it.isNull(0) }?.run { + cursor.takeIf { it.moveToFirst() && !it.isNull(0) }?.run { DataInfo( File(getString(0)), getLong(1), @@ -432,38 +432,28 @@ class AttachmentDatabase( ) ) - for (i in 0 until array.length()) { - val `object` = SaneJSONObject(array.getJSONObject(i)) - - if (!`object`.isNull(ROW_ID)) { - result.add( - DatabaseAttachment( - AttachmentId( - `object`.getLong(ROW_ID), `object`.getLong( - UNIQUE_ID - ) - ), - `object`.getLong(MMS_ID), - !TextUtils.isEmpty(`object`.getString(DATA)), - !TextUtils.isEmpty(`object`.getString(THUMBNAIL)), - `object`.getString(CONTENT_TYPE), - `object`.getInt(TRANSFER_STATE), - `object`.getLong(SIZE), - `object`.getString(FILE_NAME), - `object`.getString(CONTENT_LOCATION), - `object`.getString(CONTENT_DISPOSITION), - `object`.getString(NAME), - null, - `object`.getString(FAST_PREFLIGHT_ID), - `object`.getInt(VOICE_NOTE) == 1, - `object`.getInt(WIDTH), - `object`.getInt(HEIGHT), - `object`.getInt(QUOTE) == 1, - `object`.getString(CAPTION), - "" - ) - ) // TODO: Not sure if this will break something - } + result += array.asObjectSequence().filter { !it.isNull(ROW_ID) }.map { + DatabaseAttachment( + AttachmentId(it.getLong(ROW_ID), it.getLong(UNIQUE_ID)), + it.getLong(MMS_ID), + !TextUtils.isEmpty(it.getString(DATA)), + !TextUtils.isEmpty(it.getString(THUMBNAIL)), + it.getString(CONTENT_TYPE), + it.getInt(TRANSFER_STATE), + it.getLong(SIZE), + it.getString(FILE_NAME), + it.getString(CONTENT_LOCATION), + it.getString(CONTENT_DISPOSITION), + it.getString(NAME), + null, + it.getString(FAST_PREFLIGHT_ID), + it.getInt(VOICE_NOTE) == 1, + it.getInt(WIDTH), + it.getInt(HEIGHT), + it.getInt(QUOTE) == 1, + it.getString(CAPTION), + "" + ) } return ArrayList(result) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/JsonUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/JsonUtil.kt index 181a9f61950..f84389c4c55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/JsonUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/JsonUtil.kt @@ -3,20 +3,6 @@ package org.thoughtcrime.securesms.database import org.json.JSONArray import org.json.JSONObject -fun JSONArray.forEach(action: (JSONObject) -> Unit) { - for (i in 0 until length()) { - action(getJSONObject(i)) - } -} +fun JSONArray.asObjectSequence(): Sequence = (0 until length()).asSequence().map(::getJSONObject) -fun JSONArray.map(transform: (JSONObject) -> T): List = buildList { - for (i in 0 until length()) { - add(transform(getJSONObject(i))) - } -} - -fun JSONArray.toList(): List = buildList { - for (i in 0 until length()) { - add(getJSONObject(i)) - } -} +fun JSONArray.map(transform: JSONArray.(Int) -> T): Sequence = (0 until length()).asSequence().map { transform(it) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt index 69bc5b5d133..b94d6a54b6e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.kt @@ -17,7 +17,7 @@ class MediaDatabase(context: Context?, databaseHelper: SQLCipherOpenHelper?) : D ) { fun getGalleryMediaForThread(threadId: Long): Cursor { val database: SQLiteDatabase = databaseHelper.readableDatabase - val cursor = database.rawQuery(GALLERY_MEDIA_QUERY, arrayOf(threadId.toString() + "")) + val cursor = database.rawQuery(GALLERY_MEDIA_QUERY, arrayOf(threadId.toString())) setNotifyConversationListeners(cursor, threadId) return cursor } @@ -31,7 +31,7 @@ class MediaDatabase(context: Context?, databaseHelper: SQLCipherOpenHelper?) : D } fun getDocumentMediaForThread(threadId: Long): Cursor = - databaseHelper.readableDatabase.rawQuery(DOCUMENT_MEDIA_QUERY, arrayOf(threadId.toString() + "")) + databaseHelper.readableDatabase.rawQuery(DOCUMENT_MEDIA_QUERY, arrayOf(threadId.toString())) .also { setNotifyConversationListeners(it, threadId) } class MediaRecord private constructor( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index 68cc40fd8e6..0fe39c6d15b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -429,12 +429,14 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa cursor.getString(cursor.getColumnIndexOrThrow(SHARED_CONTACTS)) ?.takeUnless { it.isEmpty() } ?.let(::JSONArray) - ?.map { Contact.deserialize(it.toString()) }?.map { contact -> - contact.avatar?.takeIf { it.attachmentId != null }?.run { - Contact.Avatar(attachmentId, attachmentIdMap[attachmentId], isProfile) - .let { Contact(contact, it) } - } ?: contact - } ?: emptyList() + ?.map(JSONArray::getString) + ?.map(Contact::deserialize) + ?.map { contact -> + contact.avatar?.takeIf { it.attachmentId != null } + ?.run { Contact.Avatar(attachmentId, attachmentIdMap[attachmentId], isProfile) } + ?.let { Contact(contact, it) } + ?: contact + }?.toList() ?: emptyList() } catch (e: JSONException) { Log.w(TAG, "Failed to parse shared contacts.", e) emptyList() @@ -451,13 +453,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa cursor.getString(cursor.getColumnIndexOrThrow(LINK_PREVIEWS)) ?.takeUnless { it.isEmpty() } ?.let(::JSONArray) - ?.map{ it.toString() } + ?.map(JSONArray::getString) ?.map(LinkPreview::deserialize) ?.map { preview -> preview.attachmentId?.let { id -> attachmentIdMap[id]?.let { LinkPreview(preview.url, preview.title, it) } } ?: preview - } ?: emptyList() + }?.toList() ?: emptyList() } catch (e: JSONException) { Log.w(TAG, "Failed to parse shared contacts.", e) emptyList() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index ee82a708c45..039886f9e44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -583,9 +583,12 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), lifecycleScope.launch(Dispatchers.Main) { val context = this@HomeActivity // Cancel any outstanding jobs - DatabaseComponent.get(context).sessionJobDatabase().cancelPendingMessageSendJobs(threadID) + val databases = DatabaseComponent.get(context) + databases.sessionJobDatabase().cancelPendingMessageSendJobs(threadID) + val groupDatabase = databases.groupDatabase() + val lokiThreadDatabase = databases.lokiThreadDatabase() // Send a leave group message if this is an active closed group - if (recipient.address.isClosedGroup && DatabaseComponent.get(context).groupDatabase().isActive(recipient.address.toGroupString())) { + if (recipient.address.isClosedGroup && groupDatabase.isActive(recipient.address.toGroupString())) { try { GroupUtil.doubleDecodeGroupID(recipient.address.toString()).toHexString() .takeIf(DatabaseComponent.get(context).lokiAPIDatabase()::isClosedGroup) @@ -594,7 +597,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } // Delete the conversation - val v2OpenGroup = DatabaseComponent.get(context).lokiThreadDatabase().getOpenGroupChat(threadID) + val v2OpenGroup = lokiThreadDatabase.getOpenGroupChat(threadID) if (v2OpenGroup != null) { v2OpenGroup.apply { OpenGroupManager.delete(server, room, context) } } else { From 24d1560da63ca261e87142d045638c12e86c4780 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 18 Jul 2024 20:29:40 +0930 Subject: [PATCH 08/12] Simplify MmsDatabase --- .../securesms/database/MmsDatabase.kt | 197 ++++++------------ 1 file changed, 64 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index 0fe39c6d15b..6668d62ae19 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -47,7 +47,6 @@ import org.session.libsession.utilities.IdentityKeyMismatchList import org.session.libsession.utilities.NetworkFailure import org.session.libsession.utilities.NetworkFailureList import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled -import org.session.libsession.utilities.Util.toIsoBytes import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log @@ -122,20 +121,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa ).use { cursor -> while (cursor.moveToNext()) { if (MmsSmsColumns.Types.isOutgoingMessageType( - cursor.getLong( - cursor.getColumnIndexOrThrow( - MESSAGE_BOX - ) - ) + cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)) ) ) { - val theirAddress = fromSerialized( - cursor.getString( - cursor.getColumnIndexOrThrow( - ADDRESS - ) - ) - ) + val theirAddress = fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))) val ourAddress = messageId.address if (ourAddress.equals(theirAddress) || theirAddress.isGroup) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) @@ -172,9 +161,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa fun getThreadIdForMessage(id: Long): Long = databaseHelper.readableDatabase.rawQuery( "SELECT $THREAD_ID FROM $TABLE_NAME WHERE $ID = ?", arrayOf(id.toString()) - ).use { - it.takeIf { it.moveToFirst() }?.getLong(0) ?: -1 - } + ).use { it.takeIf { it.moveToFirst() }?.getLong(0) ?: -1 } private fun rawQuery(where: String, arguments: Array? = null): Cursor = databaseHelper.readableDatabase.rawQuery( "SELECT " + MMS_PROJECTION.joinToString(",") + " FROM " + TABLE_NAME + @@ -183,11 +170,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa " WHERE " + where + " GROUP BY " + TABLE_NAME + "." + ID, arguments ) - fun getMessage(messageId: Long): Cursor { - val cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString())) - setNotifyConversationListeners(cursor, getThreadIdForMessage(messageId)) - return cursor - } + fun getMessage(messageId: Long): Cursor = rawQuery(RAW_ID_WHERE, arrayOf(messageId.toString())) + .also { setNotifyConversationListeners(it, getThreadIdForMessage(messageId)) } fun getRecentChatMemberIDs(threadID: Long, limit: Int): List = databaseHelper.readableDatabase.rawQuery( """ @@ -197,9 +181,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa LIMIT $limit """.trimIndent(), threadID - ).use { cursor -> - cursor.map { it.getString(0) }.toList() - } + ).use { it.map { it.getString(0) }.toList() } val expireStartedMessages: Reader get() = readerFor(rawQuery(where = "$EXPIRE_STARTED > 0")) @@ -260,8 +242,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa override fun markUnidentified(messageId: Long, unidentified: Boolean) { val contentValues = ContentValues() contentValues.put(UNIDENTIFIED, if (unidentified) 1 else 0) - val db = databaseHelper.writableDatabase - db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) + databaseHelper.writableDatabase.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) } override fun markAsDeleted(messageId: Long, read: Boolean, hasMention: Boolean) { @@ -375,30 +356,22 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val previewAttachments = previews.mapNotNull { it.getThumbnail().orNull() } val attachments = associatedAttachments.filterNot { it.isQuote || it in contactAttachments || it in previewAttachments } val recipient = Recipient.from(context, fromSerialized(address), false) - val quote: QuoteModel? = if (quoteId > 0 && (!quoteText.isNullOrEmpty() || quoteAttachments.isNotEmpty())) { + val quote = quoteId.takeIf { it > 0 && (!quoteText.isNullOrEmpty() || quoteAttachments.isNotEmpty()) }?.let { QuoteModel( - quoteId, + it, fromSerialized(quoteAuthor), quoteText, // TODO: refactor this to use referenced quote quoteMissing, quoteAttachments ) - } else null + } val mismatches = mismatchDocument.takeUnless{ it.isNullOrEmpty() }?.let { - try { - JsonUtil.fromJson(it, IdentityKeyMismatchList::class.java).list - } catch (e: IOException) { - Log.w(TAG, e) - emptyList() - } + runCatching { JsonUtil.fromJson(it, IdentityKeyMismatchList::class.java).list } + .onFailure { Log.w(TAG, it) }.getOrNull() } ?: emptyList() val networkFailures = networkDocument?.takeUnless { it.isEmpty() }?.let { - try { - JsonUtil.fromJson(it, NetworkFailureList::class.java).list - } catch (e: IOException) { - Log.w(TAG, e) - emptyList() - } + runCatching { JsonUtil.fromJson(it, NetworkFailureList::class.java).list } + .onFailure { Log.w(TAG, it) }.getOrNull() } ?: emptyList() val message = OutgoingMediaMessage( recipient, @@ -424,49 +397,28 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa private fun getSharedContacts( cursor: Cursor, attachments: List - ): List = try { + ): List = runCatching { val attachmentIdMap = attachments.associateBy { it.attachmentId } cursor.getString(cursor.getColumnIndexOrThrow(SHARED_CONTACTS)) - ?.takeUnless { it.isEmpty() } - ?.let(::JSONArray) - ?.map(JSONArray::getString) - ?.map(Contact::deserialize) - ?.map { contact -> + .let(::JSONArray).map { getString(it) }.map(Contact::deserialize) + .map { contact -> contact.avatar?.takeIf { it.attachmentId != null } ?.run { Contact.Avatar(attachmentId, attachmentIdMap[attachmentId], isProfile) } ?.let { Contact(contact, it) } ?: contact - }?.toList() ?: emptyList() - } catch (e: JSONException) { - Log.w(TAG, "Failed to parse shared contacts.", e) - emptyList() - } catch (e: IOException) { - Log.w(TAG, "Failed to parse shared contacts.", e) - emptyList() - } + }.toList() + }.onFailure { Log.w(TAG, "Failed to parse shared contacts.", it) }.getOrNull() ?: emptyList() private fun getLinkPreviews( cursor: Cursor, attachments: List - ): List = try { + ): List = runCatching { val attachmentIdMap = attachments.associateBy { it.attachmentId } cursor.getString(cursor.getColumnIndexOrThrow(LINK_PREVIEWS)) - ?.takeUnless { it.isEmpty() } - ?.let(::JSONArray) - ?.map(JSONArray::getString) - ?.map(LinkPreview::deserialize) - ?.map { preview -> - preview.attachmentId?.let { id -> - attachmentIdMap[id]?.let { LinkPreview(preview.url, preview.title, it) } - } ?: preview - }?.toList() ?: emptyList() - } catch (e: JSONException) { - Log.w(TAG, "Failed to parse shared contacts.", e) - emptyList() - } catch (e: IOException) { - Log.w(TAG, "Failed to parse shared contacts.", e) - emptyList() - } + .let(::JSONArray).map { getString(it) }.map(LinkPreview::deserialize) + .map { preview -> attachmentIdMap[preview.attachmentId]?.let { LinkPreview(preview.url, preview.title, it) } ?: preview } + .toList() + }.onFailure { Log.w(TAG, "Failed to parse link previews.", it) }.getOrNull() ?: emptyList() @Throws(MmsException::class) private fun insertMessageInbox( @@ -558,25 +510,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa threadId: Long, serverTimestamp: Long = 0, runThreadUpdate: Boolean - ): Optional { - var type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT - if (retrieved.isPushMessage) { - type = type or MmsSmsColumns.Types.PUSH_MESSAGE_BIT - } - if (retrieved.isExpirationUpdate) { - type = type or MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT - } - if (retrieved.isScreenshotDataExtraction) { - type = type or MmsSmsColumns.Types.SCREENSHOT_EXTRACTION_BIT - } - if (retrieved.isMediaSavedDataExtraction) { - type = type or MmsSmsColumns.Types.MEDIA_SAVED_EXTRACTION_BIT - } - if (retrieved.isMessageRequestResponse) { - type = type or MmsSmsColumns.Types.MESSAGE_REQUEST_RESPONSE_BIT - } - return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runThreadUpdate) - } + ): Optional = listOfNotNull( + MmsSmsColumns.Types.BASE_INBOX_TYPE, + MmsSmsColumns.Types.SECURE_MESSAGE_BIT, + MmsSmsColumns.Types.PUSH_MESSAGE_BIT.takeIf { retrieved.isPushMessage }, + MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT.takeIf { retrieved.isExpirationUpdate }, + MmsSmsColumns.Types.SCREENSHOT_EXTRACTION_BIT.takeIf { retrieved.isScreenshotDataExtraction }, + MmsSmsColumns.Types.MEDIA_SAVED_EXTRACTION_BIT.takeIf { retrieved.isMediaSavedDataExtraction }, + MmsSmsColumns.Types.MESSAGE_REQUEST_RESPONSE_BIT.takeIf { retrieved.isMessageRequestResponse } + ).reduce { type, flag -> type or flag } + .let { type -> insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runThreadUpdate) } @JvmOverloads @Throws(MmsException::class) @@ -587,16 +530,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa serverTimestamp: Long = 0, runThreadUpdate: Boolean ): Long { - var type = MmsSmsColumns.Types.BASE_SENDING_TYPE - if (message.isSecure) type = - type or (MmsSmsColumns.Types.SECURE_MESSAGE_BIT or MmsSmsColumns.Types.PUSH_MESSAGE_BIT) - if (forceSms) type = type or MmsSmsColumns.Types.MESSAGE_FORCE_SMS_BIT - if (message.isGroup && message is OutgoingGroupMediaMessage) { - if (message.isUpdateMessage) type = type or MmsSmsColumns.Types.GROUP_UPDATE_MESSAGE_BIT - } - if (message.isExpirationUpdate) { - type = type or MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT - } + val type = listOfNotNull( + MmsSmsColumns.Types.BASE_SENDING_TYPE, + (MmsSmsColumns.Types.SECURE_MESSAGE_BIT or MmsSmsColumns.Types.PUSH_MESSAGE_BIT).takeIf { message.isSecure }, + MmsSmsColumns.Types.MESSAGE_FORCE_SMS_BIT.takeIf { forceSms }, + MmsSmsColumns.Types.GROUP_UPDATE_MESSAGE_BIT.takeIf { message.isGroup && message is OutgoingGroupMediaMessage }, + MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT.takeIf { message.isExpirationUpdate } + ).reduce { type, flag -> type or flag } val earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.sentTimeMillis) val earlyReadReceipts = earlyReadReceiptCache.remove(message.sentTimeMillis) val contentValues = ContentValues() @@ -615,18 +555,12 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa contentValues.put(EXPIRES_IN, message.expiresIn) contentValues.put(EXPIRE_STARTED, message.expireStartedAt) contentValues.put(ADDRESS, message.recipient.address.serialize()) - contentValues.put( - DELIVERY_RECEIPT_COUNT, - Stream.of(earlyDeliveryReceipts.values).mapToLong { obj: Long -> obj } - .sum()) - contentValues.put( - READ_RECEIPT_COUNT, - Stream.of(earlyReadReceipts.values).mapToLong { obj: Long -> obj } - .sum()) - if (message.outgoingQuote != null) { - contentValues.put(QUOTE_ID, message.outgoingQuote!!.id) - contentValues.put(QUOTE_AUTHOR, message.outgoingQuote!!.author.serialize()) - contentValues.put(QUOTE_MISSING, if (message.outgoingQuote!!.missing) 1 else 0) + contentValues.put(DELIVERY_RECEIPT_COUNT, earlyDeliveryReceipts.values.sumOf { it.toLong() }) + contentValues.put(READ_RECEIPT_COUNT, earlyReadReceipts.values.sumOf { it.toLong() }) + message.outgoingQuote?.run { + contentValues.put(QUOTE_ID, id) + contentValues.put(QUOTE_AUTHOR, author.serialize()) + contentValues.put(QUOTE_MISSING, if (missing) 1 else 0) } val quoteAttachments = message.outgoingQuote?.attachments ?: emptyList() if (isDuplicate(message, threadId)) { @@ -663,15 +597,11 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa -1 ) } - with (get(context).threadDatabase()) { - val lastSeen = getLastSeenAndHasSent(threadId).first() - if (lastSeen < message.sentTimeMillis) { - setLastSeen(threadId, message.sentTimeMillis) - } + get(context).threadDatabase().run { + message.sentTimeMillis.takeIf { getLastSeenAndHasSent(threadId).first() < it } + ?.let { setLastSeen(threadId, it) } setHasSent(threadId, true) - if (runThreadUpdate) { - update(threadId, true, true) - } + if (runThreadUpdate) update(threadId, true, true) } return messageId } @@ -698,6 +628,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa contentValues.put(BODY, body) contentValues.put(PART_COUNT, allAttachments.size) db.beginTransaction() + db.transaction { this.transaction { } } return try { val messageId = db.insert(TABLE_NAME, null, contentValues) val insertedAttachments = partsDatabase.insertAttachmentsForMessage( @@ -705,8 +636,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa allAttachments, quoteAttachments ) - val serializedContacts = - getSerializedSharedContacts(insertedAttachments, sharedContacts) + val serializedContacts = getSerializedSharedContacts(insertedAttachments, sharedContacts) val serializedPreviews = getSerializedLinkPreviews(insertedAttachments, linkPreviews) if (!serializedContacts.isNullOrEmpty()) { val contactValues = ContentValues() @@ -755,11 +685,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } } val query = queryBuilder.toString() - val db = databaseHelper.writableDatabase val values = ContentValues(2) values.put(QUOTE_MISSING, 1) values.put(QUOTE_AUTHOR, "") - db!!.update(TABLE_NAME, values, query, null) + databaseHelper.writableDatabase.update(TABLE_NAME, values, query, null) } /** @@ -1108,13 +1037,15 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa message.subscriptionId, message.expiresIn, SnodeAPI.nowWithOffset, 0, - if (message.outgoingQuote != null) Quote( - message.outgoingQuote!!.id, - message.outgoingQuote!!.author, - message.outgoingQuote!!.text, // TODO: use the referenced message's content - message.outgoingQuote!!.missing, - SlideDeck(context, message.outgoingQuote!!.attachments!!) - ) else null, + message.outgoingQuote?.run { + Quote( + id, + author, + text, // TODO: use the referenced message's content + missing, + SlideDeck(context, attachments!!) + ) + }, message.sharedContacts, message.linkPreviews, listOf(), false, false ) } @@ -1401,7 +1332,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa private const val TEMP_TABLE_NAME = "TEMP_TABLE_NAME" - const val COMMA_SEPARATED_COLUMNS = "$ID, $THREAD_ID, $DATE_SENT, $DATE_RECEIVED, $MESSAGE_BOX, $READ, m_id, sub, sub_cs, $BODY, $PART_COUNT, ct_t, $CONTENT_LOCATION, $ADDRESS, $ADDRESS_DEVICE_ID, $EXPIRY, m_cls, $MESSAGE_TYPE, v, $MESSAGE_SIZE, pri, rr,rpt_a, resp_st, $STATUS, $TRANSACTION_ID, retr_st, retr_txt, retr_txt_cs, read_status, ct_cls, resp_txt, d_tm, $DELIVERY_RECEIPT_COUNT, $MISMATCHED_IDENTITIES, $NETWORK_FAILURE, d_rpt, $SUBSCRIPTION_ID, $EXPIRES_IN, $EXPIRE_STARTED, $NOTIFIED, $READ_RECEIPT_COUNT, $QUOTE_ID, $QUOTE_AUTHOR, $QUOTE_BODY, $QUOTE_ATTACHMENT, $QUOTE_MISSING, $SHARED_CONTACTS, $UNIDENTIFIED, $LINK_PREVIEWS, $MESSAGE_REQUEST_RESPONSE, $REACTIONS_UNREAD, $REACTIONS_LAST_SEEN, $HAS_MENTION" + private const val COMMA_SEPARATED_COLUMNS = "$ID, $THREAD_ID, $DATE_SENT, $DATE_RECEIVED, $MESSAGE_BOX, $READ, m_id, sub, sub_cs, $BODY, $PART_COUNT, ct_t, $CONTENT_LOCATION, $ADDRESS, $ADDRESS_DEVICE_ID, $EXPIRY, m_cls, $MESSAGE_TYPE, v, $MESSAGE_SIZE, pri, rr,rpt_a, resp_st, $STATUS, $TRANSACTION_ID, retr_st, retr_txt, retr_txt_cs, read_status, ct_cls, resp_txt, d_tm, $DELIVERY_RECEIPT_COUNT, $MISMATCHED_IDENTITIES, $NETWORK_FAILURE, d_rpt, $SUBSCRIPTION_ID, $EXPIRES_IN, $EXPIRE_STARTED, $NOTIFIED, $READ_RECEIPT_COUNT, $QUOTE_ID, $QUOTE_AUTHOR, $QUOTE_BODY, $QUOTE_ATTACHMENT, $QUOTE_MISSING, $SHARED_CONTACTS, $UNIDENTIFIED, $LINK_PREVIEWS, $MESSAGE_REQUEST_RESPONSE, $REACTIONS_UNREAD, $REACTIONS_LAST_SEEN, $HAS_MENTION" @JvmField val ADD_AUTOINCREMENT = arrayOf( From dc99bb83a741fdc26119050473b052d6ffe57699 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 18 Jul 2024 20:36:08 +0930 Subject: [PATCH 09/12] Improve Database --- .../securesms/database/Database.kt | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Database.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Database.kt index f51bc2101fb..46aefb3c39f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Database.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Database.kt @@ -30,19 +30,17 @@ abstract class Database @SuppressLint("WrongConstant") constructor( @JvmField protected val context: Context, @JvmField protected var databaseHelper: SQLCipherOpenHelper ) { + protected val readableDatabase: SQLiteDatabase = databaseHelper.readableDatabase + protected val writableDatabase: SQLiteDatabase = databaseHelper.writableDatabase + private val conversationListNotificationDebouncer: WindowDebouncer = - ApplicationContext.getInstance( - context - ).conversationListDebouncer + ApplicationContext.getInstance(context).conversationListDebouncer private val conversationListUpdater = Runnable { - context.contentResolver.notifyChange( - DatabaseContentProviders.ConversationList.CONTENT_URI, - null - ) + context.contentResolver.notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null) } protected fun notifyConversationListeners(threadIds: Set) { - for (threadId in threadIds) notifyConversationListeners(threadId) + threadIds.forEach(::notifyConversationListeners) } protected fun notifyConversationListeners(threadId: Long) { @@ -96,12 +94,6 @@ abstract class Database @SuppressLint("WrongConstant") constructor( this.databaseHelper = databaseHelper } - protected val readableDatabase: SQLiteDatabase - get() = databaseHelper.readableDatabase - - protected val writableDatabase: SQLiteDatabase - get() = databaseHelper.writableDatabase - companion object { const val ID_WHERE: String = "_id = ?" } From b39679d0e5ac713fbb6e6e0bf255e5c121e50a44 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 18 Jul 2024 20:36:53 +0930 Subject: [PATCH 10/12] Convert LinkPreview --- .../v2/components/LinkPreviewDraftView.kt | 4 +- .../v2/messages/LinkPreviewView.kt | 4 +- .../v2/messages/VisibleMessageContentView.kt | 2 +- .../securesms/database/MmsDatabase.kt | 10 +- .../link_preview/LinkPreview.java | 92 ------------------- .../link_preview/LinkPreview.kt | 73 +++++++++++++++ 6 files changed, 83 insertions(+), 102 deletions(-) delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java create mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt index 9c414f34fd4..8510e0dd588 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt @@ -32,9 +32,9 @@ class LinkPreviewDraftView : LinearLayout { binding.linkPreviewDraftContainer.isVisible = true binding.linkPreviewDraftLoader.isVisible = false binding.thumbnailImageView.root.setRoundedCorners(toPx(4, resources)) - if (linkPreview.getThumbnail().isPresent) { + if (linkPreview.thumbnail.isPresent) { // This internally fetches the thumbnail - binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false) + binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.thumbnail.get()), false) } binding.linkPreviewDraftTitleTextView.text = linkPreview.title } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index 4e6066edb32..a59a5235422 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -39,9 +39,9 @@ class LinkPreviewView : LinearLayout { val linkPreview = message.linkPreviews.first() url = linkPreview.url // Thumbnail - if (linkPreview.getThumbnail().isPresent) { + if (linkPreview.thumbnail.isPresent) { // This internally fetches the thumbnail - binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false) + binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.thumbnail.get()), isPreview = false) binding.thumbnailImageView.root.loadIndicator.isVisible = false } // Title diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index b320e72e265..d77a3f4d4d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -139,7 +139,7 @@ class VisibleMessageContentView : ConstraintLayout { onAttachmentNeedsDownload(dbAttachment) } message.linkPreviews.forEach { preview -> - val previewThumbnail = preview.getThumbnail().orNull() as? DatabaseAttachment ?: return@forEach + val previewThumbnail = preview.thumbnail.orNull() as? DatabaseAttachment ?: return@forEach onAttachmentNeedsDownload(previewThumbnail) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index 6668d62ae19..e07d2309294 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -353,7 +353,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val contacts = getSharedContacts(cursor, associatedAttachments) val contactAttachments: Set = contacts.mapNotNull { it.avatarAttachment }.toSet() val previews = getLinkPreviews(cursor, associatedAttachments) - val previewAttachments = previews.mapNotNull { it.getThumbnail().orNull() } + val previewAttachments = previews.mapNotNull { it.thumbnail.orNull() } val attachments = associatedAttachments.filterNot { it.isQuote || it in contactAttachments || it in previewAttachments } val recipient = Recipient.from(context, fromSerialized(address), false) val quote = quoteId.takeIf { it > 0 && (!quoteText.isNullOrEmpty() || quoteAttachments.isNotEmpty()) }?.let { @@ -619,7 +619,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val db = databaseHelper.writableDatabase val partsDatabase = get(context).attachmentDatabase() val contactAttachments = sharedContacts.mapNotNull { it.avatarAttachment } - val previewAttachments = linkPreviews.mapNotNull { it.getThumbnail().orNull() } + val previewAttachments = linkPreviews.mapNotNull { it.thumbnail.orNull() } val allAttachments = buildList { addAll(attachments) addAll(contactAttachments) @@ -819,8 +819,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa for (preview in previews) { try { var attachmentId: AttachmentId? = null - if (preview!!.getThumbnail().isPresent) { - attachmentId = insertedAttachmentIds[preview.getThumbnail().get()] + if (preview!!.thumbnail.isPresent) { + attachmentId = insertedAttachmentIds[preview.thumbnail.get()] } val updatedPreview = LinkPreview( preview.url, preview.title, attachmentId @@ -1121,7 +1121,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val contactAttachments: Set = contacts.mapNotNull { it.avatarAttachment }.toSet() val previews: List = getLinkPreviews(cursor, attachments) val previewAttachments: Set = - previews.mapNotNull { it?.getThumbnail()?.orNull() }.toSet() + previews.mapNotNull { it?.thumbnail?.orNull() }.toSet() val slideDeck = getSlideDeck( attachments.filterNot { it in contactAttachments || it in previewAttachments } ) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java deleted file mode 100644 index ae5376b79d0..00000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.session.libsession.messaging.sending_receiving.link_preview; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; - -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId; -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsignal.utilities.JsonUtil; -import org.session.libsignal.utilities.guava.Optional; - -import java.io.IOException; -import java.util.Objects; - -public class LinkPreview { - - @JsonProperty - private final String url; - - @JsonProperty - private final String title; - - @JsonProperty - private final AttachmentId attachmentId; - - @JsonIgnore - public Optional thumbnail; - - public LinkPreview(@NonNull String url, @NonNull String title, @NonNull DatabaseAttachment thumbnail) { - this.url = url; - this.title = title; - this.thumbnail = Optional.of(thumbnail); - this.attachmentId = thumbnail.attachmentId; - } - - public LinkPreview(@NonNull String url, @NonNull String title, @NonNull Optional thumbnail) { - this.url = url; - this.title = title; - this.thumbnail = thumbnail; - this.attachmentId = null; - } - - public LinkPreview(@JsonProperty("url") @NonNull String url, - @JsonProperty("title") @NonNull String title, - @JsonProperty("attachmentId") @Nullable AttachmentId attachmentId) - { - this.url = url; - this.title = title; - this.attachmentId = attachmentId; - this.thumbnail = Optional.absent(); - } - - public String getUrl() { - return url; - } - - public String getTitle() { - return title; - } - - public Optional getThumbnail() { - return thumbnail; - } - - public @Nullable AttachmentId getAttachmentId() { - return attachmentId; - } - - public String serialize() throws IOException { - return JsonUtil.toJsonThrows(this); - } - - public static LinkPreview deserialize(@NonNull String serialized) throws IOException { - return JsonUtil.fromJson(serialized, LinkPreview.class); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - LinkPreview that = (LinkPreview) o; - return Objects.equals(url, that.url) && Objects.equals(title, that.title) && Objects.equals(attachmentId, that.attachmentId) && Objects.equals(thumbnail, that.thumbnail); - } - - @Override - public int hashCode() { - return Objects.hash(url, title, attachmentId, thumbnail); - } -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt new file mode 100644 index 00000000000..c54084cb321 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt @@ -0,0 +1,73 @@ +package org.session.libsession.messaging.sending_receiving.link_preview + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import org.session.libsession.messaging.sending_receiving.attachments.Attachment +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsignal.utilities.JsonUtil +import org.session.libsignal.utilities.guava.Optional +import java.io.IOException +import java.util.Objects + +class LinkPreview { + @JsonProperty + val url: String + + @JsonProperty + val title: String + + @JsonProperty + val attachmentId: AttachmentId? + + @JsonIgnore + var thumbnail: Optional + + constructor(url: String, title: String, thumbnail: DatabaseAttachment) { + this.url = url + this.title = title + this.thumbnail = Optional.of(thumbnail) + this.attachmentId = thumbnail.attachmentId + } + + constructor(url: String, title: String, thumbnail: Optional) { + this.url = url + this.title = title + this.thumbnail = thumbnail + this.attachmentId = null + } + + constructor( + @JsonProperty("url") url: String, + @JsonProperty("title") title: String, + @JsonProperty("attachmentId") attachmentId: AttachmentId? + ) { + this.url = url + this.title = title + this.attachmentId = attachmentId + this.thumbnail = Optional.absent() + } + + @Throws(IOException::class) + fun serialize(): String { + return JsonUtil.toJsonThrows(this) + } + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val that = o as LinkPreview + return url == that.url && title == that.title && attachmentId == that.attachmentId && thumbnail == that.thumbnail + } + + override fun hashCode(): Int { + return Objects.hash(url, title, attachmentId, thumbnail) + } + + companion object { + @Throws(IOException::class) + fun deserialize(serialized: String): LinkPreview { + return JsonUtil.fromJson(serialized, LinkPreview::class.java) + } + } +} From 3f2c288a5d9f91b4b5685f5bb67f2fe2f9ca7822 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 18 Jul 2024 20:44:59 +0930 Subject: [PATCH 11/12] Simplify LinkPreview --- .../link_preview/LinkPreview.kt | 59 ++++--------------- 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt index c54084cb321..9ae654a7787 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt @@ -6,63 +6,24 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.utilities.guava.Optional import java.io.IOException -import java.util.Objects - -class LinkPreview { - @JsonProperty - val url: String - - @JsonProperty - val title: String - - @JsonProperty - val attachmentId: AttachmentId? - - @JsonIgnore - var thumbnail: Optional - - constructor(url: String, title: String, thumbnail: DatabaseAttachment) { - this.url = url - this.title = title - this.thumbnail = Optional.of(thumbnail) - this.attachmentId = thumbnail.attachmentId - } - - constructor(url: String, title: String, thumbnail: Optional) { - this.url = url - this.title = title - this.thumbnail = thumbnail - this.attachmentId = null - } +data class LinkPreview( + @JsonProperty val url: String, + @JsonProperty val title: String, + @JsonProperty val attachmentId: AttachmentId?, + @JsonIgnore var thumbnail: Attachment? +) { + constructor(url: String, title: String, thumbnail: DatabaseAttachment): this(url, title, thumbnail.attachmentId, thumbnail) + constructor(url: String, title: String, thumbnail: Attachment?): this(url, title, null, thumbnail) constructor( @JsonProperty("url") url: String, @JsonProperty("title") title: String, @JsonProperty("attachmentId") attachmentId: AttachmentId? - ) { - this.url = url - this.title = title - this.attachmentId = attachmentId - this.thumbnail = Optional.absent() - } + ): this(url, title, attachmentId, null) @Throws(IOException::class) - fun serialize(): String { - return JsonUtil.toJsonThrows(this) - } - - override fun equals(o: Any?): Boolean { - if (this === o) return true - if (o == null || javaClass != o.javaClass) return false - val that = o as LinkPreview - return url == that.url && title == that.title && attachmentId == that.attachmentId && thumbnail == that.thumbnail - } - - override fun hashCode(): Int { - return Objects.hash(url, title, attachmentId, thumbnail) - } + fun serialize(): String = JsonUtil.toJsonThrows(this) companion object { @Throws(IOException::class) From 80c2ba04433929ddf7765c0c835d567001489993 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 18 Jul 2024 23:00:28 +0930 Subject: [PATCH 12/12] Simplify MmsDatabase --- .../v2/components/LinkPreviewDraftView.kt | 4 +- .../v2/messages/LinkPreviewView.kt | 4 +- .../v2/messages/VisibleMessageContentView.kt | 11 +- .../securesms/database/AttachmentDatabase.kt | 213 ++++++-------- .../securesms/database/MmsDatabase.kt | 267 ++++++------------ .../linkpreview/LinkPreviewRepository.java | 4 +- .../ReceivedMessageHandler.kt | 28 +- .../attachments/PointerAttachment.java | 8 +- .../link_preview/LinkPreview.kt | 3 +- 9 files changed, 205 insertions(+), 337 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt index 8510e0dd588..19973a41d58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt @@ -32,9 +32,9 @@ class LinkPreviewDraftView : LinearLayout { binding.linkPreviewDraftContainer.isVisible = true binding.linkPreviewDraftLoader.isVisible = false binding.thumbnailImageView.root.setRoundedCorners(toPx(4, resources)) - if (linkPreview.thumbnail.isPresent) { + linkPreview.thumbnail?.let { // This internally fetches the thumbnail - binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.thumbnail.get()), false) + binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, it), false) } binding.linkPreviewDraftTitleTextView.text = linkPreview.title } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index a59a5235422..b707926fc88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -39,9 +39,9 @@ class LinkPreviewView : LinearLayout { val linkPreview = message.linkPreviews.first() url = linkPreview.url // Thumbnail - if (linkPreview.thumbnail.isPresent) { + linkPreview.thumbnail?.let { // This internally fetches the thumbnail - binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.thumbnail.get()), isPreview = false) + binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, it), isPreview = false) binding.thumbnailImageView.root.loadIndicator.isVisible = false } // Title diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index d77a3f4d4d5..8b588219fd6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -134,14 +134,9 @@ class VisibleMessageContentView : ConstraintLayout { } if (message is MmsMessageRecord) { - message.slideDeck.asAttachments().forEach { attach -> - val dbAttachment = attach as? DatabaseAttachment ?: return@forEach - onAttachmentNeedsDownload(dbAttachment) - } - message.linkPreviews.forEach { preview -> - val previewThumbnail = preview.thumbnail.orNull() as? DatabaseAttachment ?: return@forEach - onAttachmentNeedsDownload(previewThumbnail) - } + message.slideDeck.asAttachments().asSequence() + message.linkPreviews.map { it.thumbnail } + .filterIsInstance() + .forEach(onAttachmentNeedsDownload) } when { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt index 1cf1a164410..04412c0de84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.kt @@ -38,7 +38,6 @@ import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.Util.copy import org.session.libsession.utilities.Util.newSingleThreadedLifoExecutor import org.session.libsignal.utilities.ExternalStorageUtil.getCleanFileName -import org.session.libsignal.utilities.JsonUtil.SaneJSONObject import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.crypto.AttachmentSecret import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream @@ -52,7 +51,6 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.mms.MediaStream import org.thoughtcrime.securesms.mms.MmsException import org.thoughtcrime.securesms.mms.PartAuthority -import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData @@ -62,7 +60,6 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream -import java.util.TreeSet import java.util.concurrent.Callable import java.util.concurrent.ExecutionException @@ -90,7 +87,7 @@ class AttachmentDatabase( } } - fun getAttachment(attachmentId: AttachmentId): DatabaseAttachment? = databaseHelper.readableDatabase.query( + fun getAttachment(attachmentId: AttachmentId): DatabaseAttachment? = readableDatabase.query( TABLE_NAME, PROJECTION, ROW_ID_WHERE, @@ -100,15 +97,14 @@ class AttachmentDatabase( null ).use { it.takeIf(Cursor::moveToFirst)?.let(::getAttachment)?.firstOrNull() } - fun getAttachmentsForMessage(mmsId: Long) = databaseHelper.readableDatabase.query( + fun getAttachmentsForMessage(mmsId: Long) = readableDatabase.query( TABLE_NAME, PROJECTION, "$MMS_ID = ?", arrayOf(mmsId.toString()), null, null, null - ).use { cursor -> cursor.map(::getAttachment).flatMap { it }.filterNot { it.isQuote }.toList() } + ).use { cursor -> cursor.map(::getAttachment).flatten().filterNot { it.isQuote }.toList() } - fun deleteAttachmentsForMessages(messageIds: Array) { + fun deleteAttachmentsForMessages(messageIds: Array) { val idsAsString = messageIds.map { "$MMS_ID = $it" }.joinToString { " OR " } - val database: SQLiteDatabase = databaseHelper.readableDatabase - database.query( + readableDatabase.query( TABLE_NAME, arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), idsAsString, @@ -126,14 +122,12 @@ class AttachmentDatabase( } }.let(::deleteAttachmentsOnDisk) - database.delete(TABLE_NAME, idsAsString, null) + readableDatabase.delete(TABLE_NAME, idsAsString, null) notifyAttachmentListeners() } fun deleteAttachmentsForMessage(mmsId: Long) { - val database: SQLiteDatabase = databaseHelper.writableDatabase - - database.query( + writableDatabase.query( TABLE_NAME, arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), "$MMS_ID = ?", arrayOf(mmsId.toString()), null, null, null ).use { @@ -142,14 +136,12 @@ class AttachmentDatabase( } } - database.delete(TABLE_NAME, "$MMS_ID = ?", arrayOf(mmsId.toString())) + writableDatabase.delete(TABLE_NAME, "$MMS_ID = ?", arrayOf(mmsId.toString())) notifyAttachmentListeners() } fun deleteAttachment(id: AttachmentId) { - val database: SQLiteDatabase = databaseHelper.writableDatabase - - database.query( + writableDatabase.query( TABLE_NAME, arrayOf(DATA, THUMBNAIL, CONTENT_TYPE), PART_ID_WHERE, @@ -166,7 +158,7 @@ class AttachmentDatabase( val thumbnail = cursor.getString(1) val contentType = cursor.getString(2) - database.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings()) + writableDatabase.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings()) deleteAttachmentOnDisk(data, thumbnail, contentType) notifyAttachmentListeners() } @@ -221,7 +213,7 @@ class AttachmentDatabase( values.put(FAST_PREFLIGHT_ID, null as String?) values.put(URL, "") - if (databaseHelper.writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) { + if (writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) { dataInfo.file.delete() } else { notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(mmsId)) @@ -243,13 +235,13 @@ class AttachmentDatabase( values.put(FAST_PREFLIGHT_ID, attachment.fastPreflightId) values.put(URL, attachment.url) - databaseHelper.writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) + writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) } fun handleFailedAttachmentUpload(id: AttachmentId) { val values = ContentValues() values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) - databaseHelper.writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) + writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()) } @Throws(MmsException::class) @@ -303,7 +295,7 @@ class AttachmentDatabase( contentValues.put(HEIGHT, mediaStream.height) contentValues.put(DATA_RANDOM, dataInfo.random) - databaseHelper.writableDatabase.update( + writableDatabase.update( TABLE_NAME, contentValues, PART_ID_WHERE, @@ -337,7 +329,7 @@ class AttachmentDatabase( val values = ContentValues(1) values.put(TRANSFER_STATE, transferState) - databaseHelper.writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) + writableDatabase.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) notifyConversationListeners(get(context).mmsDatabase().getThreadIdForMessage(messageId)) } @@ -368,7 +360,7 @@ class AttachmentDatabase( else -> throw AssertionError("Unknown data type: $dataType") } - return databaseHelper.readableDatabase.query( + return readableDatabase.query( TABLE_NAME, arrayOf(dataType, SIZE, randomColumn), PART_ID_WHERE, @@ -415,24 +407,13 @@ class AttachmentDatabase( throw MmsException(e) } - fun getAttachment(cursor: Cursor): List { - try { - if (cursor.getColumnIndex(ATTACHMENT_JSON_ALIAS) != -1) { - if (cursor.isNull(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) { - return emptyList() - } - - val result: MutableSet = - TreeSet { o1: DatabaseAttachment, o2: DatabaseAttachment -> if (o1.attachmentId == o2.attachmentId) 0 else 1 } - val array = JSONArray( - cursor.getString( - cursor.getColumnIndexOrThrow( - ATTACHMENT_JSON_ALIAS - ) - ) - ) - - result += array.asObjectSequence().filter { !it.isNull(ROW_ID) }.map { + fun getAttachment(cursor: Cursor): List = try { + if (cursor.getColumnIndex(ATTACHMENT_JSON_ALIAS) != -1) { + cursor.takeUnless { it.isNull(it.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS)) } + ?.let { JSONArray(it.getString(it.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) } + ?.asObjectSequence() + ?.filter { !it.isNull(ROW_ID) } + ?.map { DatabaseAttachment( AttachmentId(it.getLong(ROW_ID), it.getLong(UNIQUE_ID)), it.getLong(MMS_ID), @@ -454,41 +435,36 @@ class AttachmentDatabase( it.getString(CAPTION), "" ) - } - - return ArrayList(result) - } else { - val urlIndex = cursor.getColumnIndex(URL) - return listOf( - DatabaseAttachment( - AttachmentId( - cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), - cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID)) - ), - cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), - !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), - !cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), - cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), - cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), - cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), - cursor.getString(cursor.getColumnIndexOrThrow(NAME)), - cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)), - cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), - cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, - cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), - cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)), - cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1, - cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)), - if (urlIndex > 0) cursor.getString(urlIndex) else "" - ) - ) - } - } catch (e: JSONException) { - throw AssertionError(e) - } + }?.distinctBy { it.attachmentId } + ?.toList() ?: emptyList() + } else listOf( + DatabaseAttachment( + AttachmentId( + cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), + cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID)) + ), + cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), + !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), + !cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), + cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), + cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), + cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), + cursor.getString(cursor.getColumnIndexOrThrow(NAME)), + cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)), + cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), + cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, + cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), + cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)), + cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1, + cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)), + cursor.getColumnIndex(URL).takeIf { it > 0 }?.let(cursor::getString) ?: "" + ) + ) + } catch (e: JSONException) { + throw AssertionError(e) } @@ -500,47 +476,44 @@ class AttachmentDatabase( ): AttachmentId { Log.d(TAG, "Inserting attachment for mms id: $mmsId") - var dataInfo: DataInfo? = null val uniqueId = System.currentTimeMillis() - if (attachment.dataUri != null) { - dataInfo = setAttachmentData(attachment.dataUri!!) - Log.d(TAG, "Wrote part to file: " + dataInfo.file.absolutePath) - } - - val contentValues = ContentValues() - contentValues.put(MMS_ID, mmsId) - contentValues.put(CONTENT_TYPE, attachment.contentType) - contentValues.put(TRANSFER_STATE, attachment.transferState) - contentValues.put(UNIQUE_ID, uniqueId) - contentValues.put(CONTENT_LOCATION, attachment.location) - contentValues.put(DIGEST, attachment.digest) - contentValues.put(CONTENT_DISPOSITION, attachment.key) - contentValues.put(NAME, attachment.relay) - contentValues.put(FILE_NAME, getCleanFileName(attachment.fileName)) - contentValues.put(SIZE, attachment.size) - contentValues.put(FAST_PREFLIGHT_ID, attachment.fastPreflightId) - contentValues.put(VOICE_NOTE, if (attachment.isVoiceNote) 1 else 0) - contentValues.put(WIDTH, attachment.width) - contentValues.put(HEIGHT, attachment.height) - contentValues.put(QUOTE, quote) - contentValues.put(CAPTION, attachment.caption) - contentValues.put(URL, attachment.url) - - dataInfo?.run { - contentValues.put(DATA, file.absolutePath) - contentValues.put(SIZE, length) - contentValues.put(DATA_RANDOM, random) + val dataInfo = attachment.dataUri + ?.let { setAttachmentData(it) } + ?.also { Log.d(TAG, "Wrote part to file: " + it.file.absolutePath) } + + val contentValues = ContentValues().apply { + put(MMS_ID, mmsId) + put(CONTENT_TYPE, attachment.contentType) + put(TRANSFER_STATE, attachment.transferState) + put(UNIQUE_ID, uniqueId) + put(CONTENT_LOCATION, attachment.location) + put(DIGEST, attachment.digest) + put(CONTENT_DISPOSITION, attachment.key) + put(NAME, attachment.relay) + put(FILE_NAME, getCleanFileName(attachment.fileName)) + put(SIZE, attachment.size) + put(FAST_PREFLIGHT_ID, attachment.fastPreflightId) + put(VOICE_NOTE, if (attachment.isVoiceNote) 1 else 0) + put(WIDTH, attachment.width) + put(HEIGHT, attachment.height) + put(QUOTE, quote) + put(CAPTION, attachment.caption) + put(URL, attachment.url) + dataInfo?.let { + put(DATA, it.file.absolutePath) + put(SIZE, it.length) + put(DATA_RANDOM, it.random) + } } - val rowId = databaseHelper.writableDatabase.insert(TABLE_NAME, null, contentValues) + val rowId = writableDatabase.insert(TABLE_NAME, null, contentValues) val attachmentId = AttachmentId(rowId, uniqueId) val thumbnailUri = attachment.thumbnailUri - var hasThumbnail = false - if (thumbnailUri != null) { - try { - PartAuthority.getAttachmentStream(context, thumbnailUri).use { attachmentStream -> + val hasThumbnail = thumbnailUri?.let { + runCatching { + PartAuthority.getAttachmentStream(context, it).use { attachmentStream -> val dimens = if (attachment.contentType == MediaTypes.IMAGE_GIF) { Pair(attachment.width, attachment.height) } else { @@ -548,17 +521,13 @@ class AttachmentDatabase( } updateAttachmentThumbnail( attachmentId, - PartAuthority.getAttachmentStream(context, thumbnailUri), + PartAuthority.getAttachmentStream(context, it), dimens.first.toFloat() / dimens.second.toFloat() ) - hasThumbnail = true + true } - } catch (e: IOException) { - Log.w(TAG, "Failed to save existing thumbnail.", e) - } catch (e: BitmapDecodingException) { - Log.w(TAG, "Failed to save existing thumbnail.", e) - } - } + }.onFailure { Log.w(TAG, "Failed to save existing thumbnail.", it) } + }?.getOrNull() ?: false if (!hasThumbnail && dataInfo != null) { if (MediaUtil.hasVideoThumbnail(attachment.dataUri)) { @@ -598,7 +567,7 @@ class AttachmentDatabase( val thumbnailFile = setAttachmentData(inputStream) - val database: SQLiteDatabase = databaseHelper.writableDatabase + val database: SQLiteDatabase = writableDatabase val values = ContentValues(3) values.put(THUMBNAIL, thumbnailFile.file.absolutePath) @@ -632,7 +601,7 @@ class AttachmentDatabase( */ @Synchronized fun getAttachmentAudioExtras(attachmentId: AttachmentId): DatabaseAttachmentAudioExtras? = - databaseHelper.readableDatabase // We expect all the audio extra values to be present (not null) or reject the whole record. + readableDatabase // We expect all the audio extra values to be present (not null) or reject the whole record. .query( TABLE_NAME, PROJECTION_AUDIO_EXTRAS, @@ -661,7 +630,7 @@ class AttachmentDatabase( values.put(AUDIO_VISUAL_SAMPLES, extras.visualSamples) values.put(AUDIO_DURATION, extras.durationMs) - val alteredRows: Int = databaseHelper.writableDatabase.update( + val alteredRows: Int = writableDatabase.update( TABLE_NAME, values, "$PART_ID_WHERE AND $PART_AUDIO_ONLY_WHERE", diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index e07d2309294..7247f4cb82f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -20,7 +20,6 @@ import android.content.ContentValues import android.content.Context import android.database.Cursor import androidx.sqlite.db.transaction -import com.annimon.stream.Stream import com.google.android.mms.pdu_alt.PduHeaders import org.apache.commons.lang3.StringUtils import org.json.JSONArray @@ -42,7 +41,6 @@ import org.session.libsession.utilities.Address.Companion.UNKNOWN import org.session.libsession.utilities.Address.Companion.fromExternal import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Contact -import org.session.libsession.utilities.IdentityKeyMismatch import org.session.libsession.utilities.IdentityKeyMismatchList import org.session.libsession.utilities.NetworkFailure import org.session.libsession.utilities.NetworkFailureList @@ -63,6 +61,7 @@ import org.thoughtcrime.securesms.database.model.Quote import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.mms.MmsException import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.util.asSequence import org.thoughtcrime.securesms.util.filter import org.thoughtcrime.securesms.util.map import java.io.Closeable @@ -353,7 +352,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val contacts = getSharedContacts(cursor, associatedAttachments) val contactAttachments: Set = contacts.mapNotNull { it.avatarAttachment }.toSet() val previews = getLinkPreviews(cursor, associatedAttachments) - val previewAttachments = previews.mapNotNull { it.thumbnail.orNull() } + val previewAttachments = previews.mapNotNull { it.thumbnail } val attachments = associatedAttachments.filterNot { it.isQuote || it in contactAttachments || it in previewAttachments } val recipient = Recipient.from(context, fromSerialized(address), false) val quote = quoteId.takeIf { it > 0 && (!quoteText.isNullOrEmpty() || quoteAttachments.isNotEmpty()) }?.let { @@ -577,11 +576,11 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa insertListener, ) if (message.recipient.address.isGroup) { - val members = get(context).groupDatabase() - .getGroupMembers(message.recipient.address.toGroupString(), false) + val groupDatabase = get(context).groupDatabase() + val members = groupDatabase.getGroupMembers(message.recipient.address.toGroupString(), false) val receiptDatabase = get(context).groupReceiptDatabase() - receiptDatabase.insert(Stream.of(members).map { obj: Recipient -> obj.address } - .toList(), + receiptDatabase.insert( + members.map { it.address }, messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.sentTimeMillis ) for (address in earlyDeliveryReceipts.keys) receiptDatabase.update( @@ -619,7 +618,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val db = databaseHelper.writableDatabase val partsDatabase = get(context).attachmentDatabase() val contactAttachments = sharedContacts.mapNotNull { it.avatarAttachment } - val previewAttachments = linkPreviews.mapNotNull { it.thumbnail.orNull() } + val previewAttachments = linkPreviews.mapNotNull { it.thumbnail } val allAttachments = buildList { addAll(attachments) addAll(contactAttachments) @@ -695,7 +694,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa * Delete all the messages in single queries where possible * @param messageIds a String array representation of regularly Long types representing message IDs */ - private fun deleteMessages(messageIds: Array) { + private fun deleteMessages(messageIds: Array) { if (messageIds.isEmpty()) { Log.w(TAG, "No message Ids provided to MmsDatabase.deleteMessages - aborting delete operation!") return @@ -812,56 +811,30 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa private fun getSerializedLinkPreviews( insertedAttachmentIds: Map, - previews: List - ): String? { - if (previews.isEmpty()) return null - val linkPreviewJson = JSONArray() - for (preview in previews) { - try { - var attachmentId: AttachmentId? = null - if (preview!!.thumbnail.isPresent) { - attachmentId = insertedAttachmentIds[preview.thumbnail.get()] - } - val updatedPreview = LinkPreview( - preview.url, preview.title, attachmentId - ) - linkPreviewJson.put(JSONObject(updatedPreview.serialize())) - } catch (e: JSONException) { - Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e) - } catch (e: IOException) { - Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e) - } - } - return linkPreviewJson.toString() - } + previews: List + ): String? = previews.takeUnless { it.isEmpty() }?.mapNotNull { preview -> + runCatching { + preview.run { LinkPreview(url, title, thumbnail?.let { insertedAttachmentIds[it] }) }.serialize().let(::JSONObject) + }.onFailure { Log.w(TAG, "Failed to serialize shared contact. Skipping it.", it) } + .getOrNull() + }?.fold(JSONArray(), JSONArray::put)?.toString() private fun isDuplicateMessageRequestResponse( - message: IncomingMediaMessage?, + message: IncomingMediaMessage, threadId: Long - ): Boolean { - val database = databaseHelper.readableDatabase - val cursor: Cursor? = database!!.query( - TABLE_NAME, - null, - MESSAGE_REQUEST_RESPONSE + " = 1 AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?", - arrayOf( - message!!.from.serialize(), threadId.toString() - ), - null, - null, - null, - "1" - ) - return try { - cursor != null && cursor.moveToFirst() - } finally { - cursor?.close() - } - } + ): Boolean = databaseHelper.readableDatabase.query( + TABLE_NAME, + null, + "$MESSAGE_REQUEST_RESPONSE = 1 AND $ADDRESS = ? AND $THREAD_ID = ?", + arrayOf(message.from.serialize(), threadId.toString()), + null, + null, + null, + "1" + ).use { it.moveToFirst() } - private fun isDuplicate(message: IncomingMediaMessage?, threadId: Long): Boolean { - val database = databaseHelper.readableDatabase - val cursor: Cursor? = database!!.query( + private fun isDuplicate(message: IncomingMediaMessage?, threadId: Long): Boolean = + databaseHelper.readableDatabase.query( TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?", @@ -872,22 +845,15 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa null, null, "1" - ) - return try { - cursor != null && cursor.moveToFirst() - } finally { - cursor?.close() - } - } + ).use { it.moveToFirst() } - private fun isDuplicate(message: OutgoingMediaMessage?, threadId: Long): Boolean { - val database = databaseHelper.readableDatabase - val cursor: Cursor? = database!!.query( + private fun isDuplicate(message: OutgoingMediaMessage, threadId: Long): Boolean = + databaseHelper.readableDatabase.query( TABLE_NAME, null, - DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?", + "$DATE_SENT = ? AND $ADDRESS = ? AND $THREAD_ID = ?", arrayOf( - message!!.sentTimeMillis.toString(), + message.sentTimeMillis.toString(), message.recipient.address.serialize(), threadId.toString() ), @@ -895,61 +861,31 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa null, null, "1" - ) - return try { - cursor != null && cursor.moveToFirst() - } finally { - cursor?.close() - } - } - - fun isSent(messageId: Long): Boolean { - val database = databaseHelper.readableDatabase - database!!.query( - TABLE_NAME, - arrayOf(MESSAGE_BOX), - "$ID = ?", - arrayOf(messageId.toString()), - null, - null, - null - ).use { cursor -> - if (cursor != null && cursor.moveToNext()) { - val type = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)) - return MmsSmsColumns.Types.isSentType(type) - } - } - return false - } + ).use { it.moveToFirst() } /*package*/ private fun deleteThreads(threadIds: Set) { - val db = databaseHelper.writableDatabase val where = StringBuilder() - var cursor: Cursor? = null - for (threadId in threadIds) { - where.append(THREAD_ID).append(" = '").append(threadId).append("' OR ") + threadIds.forEach { + where.append(THREAD_ID).append(" = '").append(it).append("' OR ") } val whereString = where.substring(0, where.length - 4) - try { - cursor = db!!.query(TABLE_NAME, arrayOf(ID), whereString, null, null, null, null) - val toDeleteStringMessageIds = mutableListOf() - while (cursor.moveToNext()) { - toDeleteStringMessageIds += cursor.getLong(0).toString() - } + databaseHelper.writableDatabase.query( + TABLE_NAME, + arrayOf(ID), + whereString, + null, null, null, null).use { cursor -> + cursor.map { it.getLong(0).toString() } + // TODO: this can probably be optimized out, // currently attachmentDB uses MmsID not threadID which makes it difficult to delete // and clean up on threadID alone - toDeleteStringMessageIds.toList().chunked(50).forEach { sublist -> - deleteMessages(sublist.toTypedArray()) - } - } finally { - cursor?.close() - } + }.chunked(50).map(List::toTypedArray).forEach(::deleteMessages) + val threadDb = get(context).threadDatabase() - for (threadId in threadIds) { - val threadDeleted = threadDb.update(threadId, false, true) - notifyConversationListeners(threadId) + threadIds.forEach { + threadDb.update(it, false, true) + notifyConversationListeners(it) } notifyStickerListeners() notifyStickerPackListeners() @@ -957,30 +893,26 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa /*package*/ fun deleteMessagesInThreadBeforeDate(threadId: Long, date: Long) { - var cursor: Cursor? = null - try { - val db = databaseHelper.readableDatabase - var where = - THREAD_ID + " = ? AND (CASE (" + MESSAGE_BOX + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") " - for (outgoingType in MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES) { - where += " WHEN $outgoingType THEN $DATE_SENT < $date" - } - where += " ELSE $DATE_RECEIVED < $date END)" - cursor = db!!.query( - TABLE_NAME, - arrayOf(ID), - where, - arrayOf(threadId.toString() + ""), - null, - null, - null - ) - while (cursor != null && cursor.moveToNext()) { - Log.i("MmsDatabase", "Trimming: " + cursor.getLong(0)) - deleteMessage(cursor.getLong(0)) + var where = mutableListOf() + + where += "$THREAD_ID = ? AND (CASE ($MESSAGE_BOX & ${MmsSmsColumns.Types.BASE_TYPE_MASK}) " + for (outgoingType in MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES) { + where += " WHEN $outgoingType THEN $DATE_SENT < $date" + } + where += " ELSE $DATE_RECEIVED < $date END)" + databaseHelper.readableDatabase.query( + TABLE_NAME, + arrayOf(ID), + where.joinToString(""), + arrayOf(threadId.toString() + ""), + null, + null, + null + ).use { + it.asSequence().map{ it.getLong(0) }.forEach { + Log.i("MmsDatabase", "Trimming: $it") + deleteMessage(it) } - } finally { - cursor?.close() } } @@ -988,18 +920,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa fun readerFor(message: OutgoingMediaMessage?, threadId: Long) = OutgoingMessageReader(message, threadId) - fun setQuoteMissing(messageId: Long): Int { - val contentValues = ContentValues() - contentValues.put(QUOTE_MISSING, 1) - val database = databaseHelper.writableDatabase - return database!!.update( - TABLE_NAME, - contentValues, - "$ID = ?", - arrayOf(messageId.toString()) - ) - } - /** * @param outgoing if true only delete outgoing messages, if false only delete incoming messages, if null delete both. */ @@ -1120,8 +1040,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val contacts: List = getSharedContacts(cursor, attachments) val contactAttachments: Set = contacts.mapNotNull { it.avatarAttachment }.toSet() val previews: List = getLinkPreviews(cursor, attachments) - val previewAttachments: Set = - previews.mapNotNull { it?.thumbnail?.orNull() }.toSet() + val previewAttachments: Set = previews.mapNotNull { it?.thumbnail }.toSet() val slideDeck = getSlideDeck( attachments.filterNot { it in contactAttachments || it in previewAttachments } ) @@ -1137,42 +1056,26 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } private fun getRecipientFor(serialized: String?): Recipient { - val address: Address = if (serialized.isNullOrEmpty() || "insert-address-token" == serialized) { - UNKNOWN - } else { - fromSerialized(serialized) - } + val address: Address = serialized.takeUnless { it.isNullOrEmpty() || "insert-address-token" == it } + ?.let(::fromSerialized) ?: UNKNOWN return Recipient.from(context, address, true) } - private fun getMismatchedIdentities(document: String?): List? { - if (!document.isNullOrEmpty()) { - try { - return JsonUtil.fromJson(document, IdentityKeyMismatchList::class.java).list - } catch (e: IOException) { - Log.w(TAG, e) - } - } - return LinkedList() - } + private fun getMismatchedIdentities(document: String?) = document.takeUnless { it.isNullOrEmpty() }?.let { + runCatching { JsonUtil.fromJson(it, IdentityKeyMismatchList::class.java).list } + .onFailure { Log.w(TAG, it) }.getOrNull() + } ?: emptyList() - private fun getFailures(document: String?): List? { - if (!document.isNullOrEmpty()) { - try { - return JsonUtil.fromJson(document, NetworkFailureList::class.java).list - } catch (ioe: IOException) { - Log.w(TAG, ioe) - } - } - return LinkedList() - } + private fun getFailures(document: String?): List? = + document.takeUnless { it.isNullOrEmpty() }?.let { + runCatching { JsonUtil.fromJson(document, NetworkFailureList::class.java).list } + .onFailure { Log.w(TAG, it) }.getOrNull() + } ?: emptyList() - private fun getSlideDeck(attachments: List): SlideDeck? { - val messageAttachments: List? = Stream.of(attachments) - .filterNot { obj: DatabaseAttachment? -> obj!!.isQuote } - .toList() - return SlideDeck(context, messageAttachments!!) - } + private fun getSlideDeck(attachments: List) = SlideDeck( + context, + attachments.filterNot { it.isQuote } + ) private fun getQuote(cursor: Cursor): Quote? { val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)) @@ -1183,8 +1086,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa val quoteMissing = retrievedQuote == null val quoteDeck = ( (retrievedQuote as? MmsMessageRecord)?.slideDeck ?: - Stream.of(get(context).attachmentDatabase().getAttachment(cursor)) - .filter { obj: DatabaseAttachment? -> obj!!.isQuote } + get(context).attachmentDatabase().getAttachment(cursor) + .filter { it.isQuote } .toList() .let { SlideDeck(context, it) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 6dcc928c996..ed1cf765c07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -68,7 +68,7 @@ RequestController getLinkPreview(@NonNull Context context, @NonNull String url, } if (!metadata.getImageUrl().isPresent()) { - callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().get(), Optional.absent()))); + callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().get()))); return; } @@ -76,7 +76,7 @@ RequestController getLinkPreview(@NonNull Context context, @NonNull String url, if (!metadata.getTitle().isPresent() && !attachment.isPresent()) { callback.onComplete(Optional.absent()); } else { - callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().or(""), attachment))); + callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().or(""), attachment.orNull()))); } }); diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 7bc47931d6f..a8ef1323899 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -1,7 +1,5 @@ package org.session.libsession.messaging.sending_receiving -import android.text.TextUtils -import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.util.ExpiryMode import org.session.libsession.avatars.AvatarHelper import org.session.libsession.messaging.MessagingModuleConfiguration @@ -51,7 +49,6 @@ import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.removingIdPrefixIfNeeded import org.session.libsignal.utilities.toHexString import java.security.MessageDigest @@ -355,6 +352,8 @@ fun MessageReceiver.handleVisibleMessage( if (message.quote != null && proto.dataMessage.hasQuote()) { val quote = proto.dataMessage.quote + val author2 = quote.author.takeUnless { quote.author == userBlindedKey }?.let(Address::fromSerialized) ?: Address.fromSerialized(userPublicKey!!) + val author = if (quote.author == userBlindedKey) { Address.fromSerialized(userPublicKey!!) } else { @@ -372,21 +371,20 @@ fun MessageReceiver.handleVisibleMessage( } } // Parse link preview if needed - val linkPreviews: MutableList = mutableListOf() - if (message.linkPreview != null && proto.dataMessage.previewCount > 0) { - for (preview in proto.dataMessage.previewList) { + val linkPreviews = proto.dataMessage.takeIf { (message.linkPreview != null && it.previewCount > 0) }?.let { + it.previewList.mapNotNull { preview -> val thumbnail = PointerAttachment.forPointer(preview.image) - val url = Optional.fromNullable(preview.url) - val title = Optional.fromNullable(preview.title) - val hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent - if (hasContent) { - val linkPreview = LinkPreview(url.get(), title.or(""), thumbnail) - linkPreviews.add(linkPreview) - } else { - Log.w("Loki", "Discarding an invalid link preview. hasContent: $hasContent") + val title = preview.title + val hasContent = !title.isNullOrEmpty() || thumbnail != null + when { + hasContent -> LinkPreview(preview.url, title, null, thumbnail) + else -> { + Log.w("Loki", "Discarding an invalid link preview.") + null + } } } - } + } ?: emptyList() // Parse attachments if needed val attachments = proto.dataMessage.attachmentsList.map(Attachment::fromProto).filter { it.isValid() } // Cancel any typing indicators if needed diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java index 9420adc76d3..ead892ff7c1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java @@ -5,6 +5,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.esotericsoftware.kryo.util.Null; + import org.session.libsignal.utilities.guava.Optional; import org.session.libsignal.messages.SignalServiceAttachment; import org.session.libsignal.messages.SignalServiceDataMessage; @@ -115,8 +117,8 @@ public static Optional forPointer(Optional } - public static Optional forPointer(SignalServiceProtos.AttachmentPointer pointer) { - return Optional.of(new PointerAttachment(pointer.getContentType(), + public static Attachment forPointer(@Nullable SignalServiceProtos.AttachmentPointer pointer) { + return new PointerAttachment(pointer.getContentType(), AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING, (long)pointer.getSize(), pointer.getFileName(), @@ -129,7 +131,7 @@ public static Optional forPointer(SignalServiceProtos.AttachmentPoin pointer.getWidth(), pointer.getHeight(), pointer.getCaption(), - pointer.getUrl())); + pointer.getUrl()); } public static Optional forPointer(SignalServiceProtos.DataMessage.Quote.QuotedAttachment pointer) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt index 9ae654a7787..ae725adb75c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/link_preview/LinkPreview.kt @@ -14,8 +14,9 @@ data class LinkPreview( @JsonProperty val attachmentId: AttachmentId?, @JsonIgnore var thumbnail: Attachment? ) { - constructor(url: String, title: String, thumbnail: DatabaseAttachment): this(url, title, thumbnail.attachmentId, thumbnail) + constructor(url: String, title: String): this(url, title, null, null) constructor(url: String, title: String, thumbnail: Attachment?): this(url, title, null, thumbnail) + constructor(url: String, title: String, thumbnail: DatabaseAttachment): this(url, title, thumbnail.attachmentId, thumbnail) constructor( @JsonProperty("url") url: String, @JsonProperty("title") title: String,