diff --git a/app/build.gradle b/app/build.gradle index 6723dfeddb..5782c96e7d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,8 +143,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 188 -def canonicalVersionName = "1.11.0" +def canonicalVersionCode = 189 +def canonicalVersionName = "1.11.1" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, @@ -194,8 +194,8 @@ android { versionCode canonicalVersionCode * postFixSize versionName canonicalVersionName - minSdkVersion 23 - targetSdkVersion 30 + minSdkVersion androidMinimumSdkVersion + targetSdkVersion androidCompileSdkVersion multiDexEnabled = true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 32869dd1f1..a95a9baebe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -221,7 +221,12 @@ + android:parentActivityName="org.thoughtcrime.securesms.loki.activities.HomeActivity" + android:theme="@style/Theme.Session.DayNight.FlatActionBar"> + + > { + future.addListener(object : ListenableFuture.Listener> { - override fun onSuccess(result: Pair) { - val audioSlide = AudioSlide(this@ConversationActivityV2, result.first, result.second!!, MediaTypes.AUDIO_AAC, true) + override fun onSuccess(result: Pair) { + val audioSlide = AudioSlide(this@ConversationActivityV2, result.first, result.second, MediaTypes.AUDIO_AAC, true) val slideDeck = SlideDeck() slideDeck.addSlide(audioSlide) sendAttachments(slideDeck.asAttachments(), null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index cfb1e38726..f10ad10e2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -122,7 +122,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt() val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize() quoteView.bind(sender, message.body, attachments, - thread, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide) + thread, true, maxContentWidth, message.isOpenGroupInvitation, message.threadId, false, glide) // The 6 DP below is the padding the quote view applies to itself, which isn't included in the // intrinsic height calculation. val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index 2ab119950a..b03b4be278 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -110,7 +110,8 @@ class QuoteView : LinearLayout { // region Updating fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient, - isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long, glide: GlideRequests) { + isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean, threadID: Long, + isOriginalMissing: Boolean, glide: GlideRequests) { val contactDB = DatabaseFactory.getSessionContactDatabase(context) // Reduce the max body text view line count to 2 if this is a group thread because // we'll be showing the author text view and we don't want the overall quote view height @@ -128,7 +129,7 @@ class QuoteView : LinearLayout { quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context); quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage)) // Accent line / attachment preview - val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) + val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing quoteViewAccentLine.isVisible = !hasAttachments quoteViewAttachmentPreviewContainer.isVisible = hasAttachments if (!hasAttachments) { @@ -136,8 +137,7 @@ class QuoteView : LinearLayout { accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height quoteViewAccentLine.layoutParams = accentLineLayoutParams quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) - } else { - attachments!! + } else if (attachments != null) { quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme)) val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme) 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 16721b1625..e2587c9dac 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 @@ -86,8 +86,14 @@ class VisibleMessageContentView : LinearLayout { // quote view content area's start margin. This unfortunately has to be calculated manually // here to get the layout right. val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt() - quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread, - message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide) + val quoteText = if (quote.isOriginalMissing) { + context.getString(R.string.QuoteView_original_missing) + } else { + quote.text + } + quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread, + message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, + quote.isOriginalMissing, glide) mainContainer.addView(quoteView) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) ViewUtil.setPaddingTop(bodyTextView, 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index b957b0a166..894a078b30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -10,9 +10,11 @@ import android.widget.RelativeLayout import androidx.core.view.isVisible import kotlinx.android.synthetic.main.view_voice_message.view.* import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities +import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.MmsMessageRecord import java.util.concurrent.TimeUnit import kotlin.math.roundToInt @@ -44,27 +46,30 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { val audio = message.slideDeck.audioSlide!! val player = AudioSlidePlayer.createFor(context, audio, this) this.player = player - isPreparing = true - if (!audio.isPendingDownload && !audio.isInProgress) { - player.play(0.0) - } voiceMessageViewLoader.isVisible = audio.isPendingDownload val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopRightRadius(cornerRadii[1]) cornerMask.setBottomRightRadius(cornerRadii[2]) cornerMask.setBottomLeftRadius(cornerRadii[3]) + + // only process audio if downloaded + if (audio.isPendingDownload || audio.isInProgress) return + + (audio.asAttachment() as? DatabaseAttachment)?.let { attachment -> + DatabaseFactory.getAttachmentDatabase(context).getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras -> + if (audioExtras.durationMs > 0) { + duration = audioExtras.durationMs + voiceMessageViewDurationTextView.visibility = View.VISIBLE + voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs), + TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs)) + } + } + } } - override fun onPlayerStart(player: AudioSlidePlayer) { - if (!isPreparing) { return } - isPreparing = false - duration = player.duration - voiceMessageViewDurationTextView.text = String.format("%01d:%02d", - TimeUnit.MILLISECONDS.toMinutes(duration), - TimeUnit.MILLISECONDS.toSeconds(duration)) - player.stop() - } + override fun onPlayerStart(player: AudioSlidePlayer) {} override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) { if (progress == 1.0) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index ce2ddba24a..87b59132d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.mms.MmsException; @@ -881,6 +882,20 @@ public class MmsDatabase extends MessagingDatabase { } } + public void deleteQuotedFromMessages(MessageRecord toDeleteRecord) { + String query = THREAD_ID + " = ?"; + Cursor threadMmsCursor = rawQuery(query, new String[]{String.valueOf(toDeleteRecord.getThreadId())}); + Reader reader = readerFor(threadMmsCursor); + MmsMessageRecord messageRecord; + + while ((messageRecord = (MmsMessageRecord) reader.getNext()) != null) { + if (messageRecord.getQuote() != null && toDeleteRecord.getDateSent() == messageRecord.getQuote().getId()) { + setQuoteMissing(messageRecord.getId()); + } + } + reader.close(); + } + public boolean delete(long messageId) { long threadId = getThreadIdForMessage(messageId); AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); @@ -889,6 +904,12 @@ public class MmsDatabase extends MessagingDatabase { GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); groupReceiptDatabase.deleteRowsForMessage(messageId); + MessageRecord toDelete; + try (Cursor messageCursor = getMessage(messageId)) { + toDelete = readerFor(messageCursor).getNext(); + } + + deleteQuotedFromMessages(toDelete); SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); @@ -1066,6 +1087,14 @@ public class MmsDatabase extends MessagingDatabase { return new OutgoingMessageReader(message, threadId); } + public int setQuoteMissing(long messageId) { + ContentValues contentValues = new ContentValues(); + contentValues.put(QUOTE_MISSING, 1); + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + int rows = database.update(TABLE_NAME, contentValues, ID + " = ?", new String[]{ String.valueOf(messageId) }); + return rows; + } + public static class Status { public static final int DOWNLOAD_INITIALIZED = 1; public static final int DOWNLOAD_NO_CONNECTIVITY = 2; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 6706f5fe77..f66479c41f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -514,6 +514,12 @@ public class SmsDatabase extends MessagingDatabase { Log.i("MessageDatabase", "Deleting: " + messageId); SQLiteDatabase db = databaseHelper.getWritableDatabase(); long threadId = getThreadIdForMessage(messageId); + try { + SmsMessageRecord toDelete = getMessage(messageId); + DatabaseFactory.getMmsDatabase(context).deleteQuotedFromMessages(toDelete); + } catch (NoSuchMessageException e) { + Log.e(TAG, "Couldn't find message record for messageId "+messageId, e); + } db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); notifyConversationListeners(threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 15b91048e2..7fe982a9e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -27,7 +27,6 @@ import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.loki.api.OpenGroupManager import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase @@ -190,7 +189,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) { val job = DatabaseFactory.getSessionJobDatabase(context).getMessageSendJob(messageSendJobID) ?: return - JobQueue.shared.add(job) + JobQueue.shared.resumePendingSendMessage(job) } override fun isJobCanceled(job: Job): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PrepareAttachmentAudioExtrasJob.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PrepareAttachmentAudioExtrasJob.kt index 6c1a96d1de..fa92ba3d29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PrepareAttachmentAudioExtrasJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PrepareAttachmentAudioExtrasJob.kt @@ -9,10 +9,11 @@ import org.session.libsession.messaging.utilities.Data 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.DatabaseAttachmentAudioExtras +import org.session.libsession.utilities.DecodedAudio +import org.session.libsession.utilities.InputStreamMediaDataSource import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobs.BaseJob -import org.thoughtcrime.securesms.loki.utilities.DecodedAudio import org.thoughtcrime.securesms.mms.PartAuthority import java.io.InputStream import java.lang.IllegalStateException @@ -133,35 +134,4 @@ class PrepareAttachmentAudioExtrasJob : BaseJob { /** Gets dispatched once the audio extras have been updated. */ data class AudioExtrasUpdatedEvent(val attachmentId: AttachmentId) - - @RequiresApi(Build.VERSION_CODES.M) - private class InputStreamMediaDataSource: MediaDataSource { - - private val data: ByteArray - - constructor(inputStream: InputStream): super() { - this.data = inputStream.readBytes() - } - - override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { - val length: Int = data.size - if (position >= length) { - return -1 // -1 indicates EOF - } - var actualSize = size - if (position + size > length) { - actualSize -= (position + size - length).toInt() - } - System.arraycopy(data, position.toInt(), buffer, offset, actualSize) - return actualSize - } - - override fun getSize(): Long { - return data.size.toLong() - } - - override fun close() { - // We don't need to close the wrapped stream. - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt index 86028521f1..df74bd9ca0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt @@ -14,7 +14,7 @@ import android.view.ViewConfiguration import android.view.animation.DecelerateInterpolator import androidx.core.math.MathUtils import network.loki.messenger.R -import org.thoughtcrime.securesms.loki.utilities.byteToNormalizedFloat +import org.session.libsession.utilities.byteToNormalizedFloat import kotlin.math.abs import kotlin.math.max import kotlin.math.min diff --git a/build.gradle b/build.gradle index 3343145c60..884e6ed085 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,7 @@ allprojects { } project.ext { + androidMinimumSdkVersion = 23 androidCompileSdkVersion = 30 } } \ No newline at end of file diff --git a/libsession/build.gradle b/libsession/build.gradle index 6763e0cff5..8bd46532eb 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -6,6 +6,10 @@ plugins { android { compileSdkVersion androidCompileSdkVersion + defaultConfig { + minSdkVersion androidMinimumSdkVersion + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index 4cf93a429d..37d6d44e4a 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -20,6 +20,7 @@ interface MessageDataProvider { fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer? fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream) + fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long) fun isOutgoingMessage(timestamp: Long): Boolean fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) fun handleFailedAttachmentUpload(attachmentId: Long) 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 a5aaeaa429..605052f85c 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 @@ -1,16 +1,24 @@ package org.session.libsession.messaging.jobs +import android.content.ContentResolver +import android.media.MediaDataSource +import android.media.MediaExtractor import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.utilities.Data +import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.utilities.DecodedAudio import org.session.libsession.utilities.DownloadUtilities +import org.session.libsession.utilities.InputStreamMediaDataSource import org.session.libsignal.streams.AttachmentCipherInputStream import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import java.io.File import java.io.FileInputStream +import java.io.InputStream class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job { override var delegate: JobDelegate? = null @@ -37,46 +45,64 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) val storage = MessagingModuleConfiguration.shared.storage val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val handleFailure: (java.lang.Exception) -> Unit = { exception -> - if (exception == Error.NoAttachment) { + if (exception == Error.NoAttachment + || (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 400)) { messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) this.handlePermanentFailure(exception) } else { this.handleFailure(exception) } } + var tempFile: File? = null try { val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) ?: return handleFailure(Error.NoAttachment) messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID) - val tempFile = createTempFile() + tempFile = createTempFile() val threadID = storage.getThreadIdForMms(databaseMessageID) val openGroupV2 = storage.getV2OpenGroup(threadID) - val inputStream = if (openGroupV2 == null) { + if (openGroupV2 == null) { DownloadUtilities.downloadFile(tempFile, attachment.url) - // Assume we're retrieving an attachment for an open group server if the digest is not set - if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) { - FileInputStream(tempFile) - } else { - AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest) - } } else { val url = HttpUrl.parse(attachment.url)!! val fileID = url.pathSegments().last() OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let { tempFile.writeBytes(it) } - FileInputStream(tempFile) } + val inputStream = getInputStream(tempFile, attachment) + messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream) + if (attachment.contentType.startsWith("audio/")) { + // process the duration + try { + InputStreamMediaDataSource(getInputStream(tempFile, attachment)).use { mediaDataSource -> + val durationMs = (DecodedAudio.create(mediaDataSource).totalDuration / 1000.0).toLong() + messageDataProvider.updateAudioAttachmentDuration(attachment.attachmentId, durationMs) + } + } catch (e: Exception) { + Log.e("Loki", "Couldn't process audio attachment", e) + } + } tempFile.delete() handleSuccess() } catch (e: Exception) { + tempFile?.delete() return handleFailure(e) } } + private fun getInputStream(tempFile: File, attachment: DatabaseAttachment): InputStream { + // Assume we're retrieving an attachment for an open group server if the digest is not set + return if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) { + FileInputStream(tempFile) + } else { + AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest) + } + } + private fun handleSuccess() { - Log.w(AttachmentUploadJob.TAG, "Attachment downloaded successfully.") + Log.w("AttachmentDownloadJob", "Attachment downloaded successfully.") delegate?.handleJobSucceeded(this) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index e4cc76974f..332d674115 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -11,6 +11,8 @@ import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data +import org.session.libsession.utilities.DecodedAudio +import org.session.libsession.utilities.InputStreamMediaDataSource import org.session.libsession.utilities.UploadResult import org.session.libsignal.streams.AttachmentCipherOutputStream import org.session.libsignal.messages.SignalServiceAttachmentStream @@ -108,7 +110,22 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) { Log.d(TAG, "Attachment uploaded successfully.") delegate?.handleJobSucceeded(this) - MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult) + val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider + messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult) + if (attachment.contentType.startsWith("audio/")) { + // process the duration + try { + val inputStream = messageDataProvider.getAttachmentStream(attachmentID)!!.inputStream!! + InputStreamMediaDataSource(inputStream).use { mediaDataSource -> + val durationMs = (DecodedAudio.create(mediaDataSource).totalDuration / 1000.0).toLong() + messageDataProvider.getDatabaseAttachment(attachmentID)?.attachmentId?.let { attachmentId -> + messageDataProvider.updateAudioAttachmentDuration(attachmentId, durationMs) + } + } + } catch (e: Exception) { + Log.e("Loki", "Couldn't process audio attachment", e) + } + } MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID) } @@ -140,13 +157,13 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess val kryo = Kryo() kryo.isRegistrationRequired = false val serializedMessage = ByteArray(4096) - val output = Output(serializedMessage) - kryo.writeObject(output, message) + val output = Output(serializedMessage, Job.MAX_BUFFER_SIZE) + kryo.writeClassAndObject(output, message) output.close() return Data.Builder() .putLong(ATTACHMENT_ID_KEY, attachmentID) .putString(THREAD_ID_KEY, threadID) - .putByteArray(MESSAGE_KEY, serializedMessage) + .putByteArray(MESSAGE_KEY, output.toBytes()) .putString(MESSAGE_SEND_JOB_ID_KEY, messageSendJobID) .build() } @@ -157,18 +174,24 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess class Factory: Job.Factory { - override fun create(data: Data): AttachmentUploadJob { + override fun create(data: Data): AttachmentUploadJob? { val serializedMessage = data.getByteArray(MESSAGE_KEY) val kryo = Kryo() kryo.isRegistrationRequired = false val input = Input(serializedMessage) - val message = kryo.readObject(input, Message::class.java) + val message: Message + try { + message = kryo.readClassAndObject(input) as Message + } catch (e: Exception) { + Log.e("Loki","Couldn't serialize the AttachmentUploadJob", e) + return null + } input.close() return AttachmentUploadJob( - data.getLong(ATTACHMENT_ID_KEY), - data.getString(THREAD_ID_KEY)!!, - message, - data.getString(MESSAGE_SEND_JOB_ID_KEY)!! + data.getLong(ATTACHMENT_ID_KEY), + data.getString(THREAD_ID_KEY)!!, + message, + data.getString(MESSAGE_SEND_JOB_ID_KEY)!! ) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index e6803e89dc..9ced85b110 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -23,6 +23,7 @@ class JobQueue : JobDelegate { private val attachmentDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher() private val scope = GlobalScope + SupervisorJob() private val queue = Channel(UNLIMITED) + private val pendingJobIds = mutableSetOf() val timer = Timer() @@ -86,6 +87,19 @@ class JobQueue : JobDelegate { MessagingModuleConfiguration.shared.storage.persistJob(job) } + fun resumePendingSendMessage(job: Job) { + val id = job.id ?: run { + Log.e("Loki", "tried to resume pending send job with no ID") + return + } + if (!pendingJobIds.add(id)) { + Log.e("Loki","tried to re-queue pending/in-progress job") + return + } + queue.offer(job) + Log.d("Loki", "resumed pending send message $id") + } + fun resumePendingJobs() { if (hasResumedPendingJobs) { Log.d("Loki", "resumePendingJobs() should only be called once.") @@ -120,6 +134,7 @@ class JobQueue : JobDelegate { override fun handleJobSucceeded(job: Job) { val jobId = job.id ?: return MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId) + pendingJobIds.remove(jobId) } override fun handleJobFailed(job: Job, error: Exception) { @@ -169,4 +184,7 @@ class JobQueue : JobDelegate { val maxBackoff = (10 * 60).toDouble() // 10 minutes return (1000 * 0.25 * min(maxBackoff, (2.0).pow(job.failureCount))).roundToLong() } + + private fun Job.isSend() = this is MessageSendJob || this is AttachmentUploadJob + } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt index 7cfc44801f..b08fa66f34 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt @@ -94,7 +94,9 @@ class OpenGroupPollerV2(private val server: String, private val executorService: if (actualMax > 0) { storage.setLastMessageServerID(room, server, actualMax) } - JobQueue.shared.add(TrimThreadJob(threadId)) + if (messages.isNotEmpty()) { + JobQueue.shared.add(TrimThreadJob(threadId)) + } } private fun handleDeletedMessages(room: String, openGroupID: String, deletions: List) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/DecodedAudio.kt b/libsession/src/main/java/org/session/libsession/utilities/DecodedAudio.kt similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/DecodedAudio.kt rename to libsession/src/main/java/org/session/libsession/utilities/DecodedAudio.kt index 77f7f88983..ef21abe4c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/DecodedAudio.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DecodedAudio.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.session.libsession.utilities import android.media.AudioFormat import android.media.MediaCodec @@ -11,6 +11,7 @@ import androidx.annotation.RequiresApi import java.io.FileDescriptor import java.io.IOException +import java.io.InputStream import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.ShortBuffer @@ -365,4 +366,34 @@ inline fun byteToNormalizedFloat(value: Byte): Float { /** Turns a [0..1] float into a signed byte. */ inline fun normalizedFloatToByte(value: Float): Byte { return (255f * value - 128f).roundToInt().toByte() +} + +class InputStreamMediaDataSource: MediaDataSource { + + private val data: ByteArray + + constructor(inputStream: InputStream): super() { + this.data = inputStream.readBytes() + } + + override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { + val length: Int = data.size + if (position >= length) { + return -1 // -1 indicates EOF + } + var actualSize = size + if (position + size > length) { + actualSize -= (position + size - length).toInt() + } + System.arraycopy(data, position.toInt(), buffer, offset, actualSize) + return actualSize + } + + override fun getSize(): Long { + return data.size.toLong() + } + + override fun close() { + // We don't need to close the wrapped stream. + } } \ No newline at end of file