diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index 98f856d8c8..0d7df53089 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -114,10 +114,6 @@ interface StorageProtocol { fun getReceivedMessageTimestamps(): Set fun addReceivedMessageTimestamp(timestamp: Long) fun removeReceivedMessageTimestamps(timestamps: Set) - /** - * Returns the IDs of the saved attachments. - */ - fun persistAttachments(messageID: Long, attachments: List): List fun getAttachmentsForMessage(mmsMessageId: Long): List fun getMessageBy(timestamp: Long, author: String): MessageRecord? fun updateSentTimestamp(messageId: MessageId, openGroupSentTimestamp: Long, threadId: Long) diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 88d9d5dd34..373d3274d9 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -116,15 +116,13 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult) - // Outgoing voice messages do not have their final duration set because older Android versions (API 28 and below) - // can have bugs where the media duration is calculated incorrectly. In such cases we leave the correct "interim" - // voice message duration as the final duration as we know that it'll be correct.. + // We don't need to calculate the duration for voice notes, as they will have it set already. if (attachment.contentType.startsWith("audio/") && !attachment.voiceNote) { - // ..but for outgoing audio files we do process the duration to the best of our ability. try { val inputStream = messageDataProvider.getAttachmentStream(attachmentID)!!.inputStream!! InputStreamMediaDataSource(inputStream).use { mediaDataSource -> val durationMS = (DecodedAudio.create(mediaDataSource).totalDurationMicroseconds / 1000.0).toLong() + Log.d(TAG, "Audio attachment duration calculated as: $durationMS ms") messageDataProvider.getDatabaseAttachment(attachmentID)?.attachmentId?.let { attachmentId -> messageDataProvider.updateAudioAttachmentDuration(attachmentId, durationMS, threadID.toLong()) } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/Attachment.java b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/Attachment.java index c9438bb7cf..e0cf2c28d6 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/Attachment.java +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/Attachment.java @@ -6,43 +6,53 @@ public abstract class Attachment { - @NonNull private final String contentType; + @NonNull + private final String contentType; private final int transferState; private final long size; private final String filename; - @Nullable private final String location; - @Nullable private final String key; - @Nullable private final String relay; - @Nullable private final byte[] digest; - @Nullable private final String fastPreflightId; + @Nullable + private final String location; + @Nullable + private final String key; + @Nullable + private final String relay; + @Nullable + private final byte[] digest; + @Nullable + private final String fastPreflightId; private final boolean voiceNote; private final int width; private final int height; private final boolean quote; - @Nullable private final String caption; + @Nullable + private final String caption; private final String url; + private final long audioDurationMs; + public Attachment(@NonNull String contentType, int transferState, long size, String filename, @Nullable String location, @Nullable String key, @Nullable String relay, @Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote, - int width, int height, boolean quote, @Nullable String caption, String url) - { - this.contentType = contentType; - this.transferState = transferState; - this.size = size; - this.filename = filename; - this.location = location; - this.key = key; - this.relay = relay; - this.digest = digest; + int width, int height, boolean quote, @Nullable String caption, String url, + long audioDurationMs) { + this.contentType = contentType; + this.transferState = transferState; + this.size = size; + this.filename = filename; + this.location = location; + this.key = key; + this.relay = relay; + this.digest = digest; this.fastPreflightId = fastPreflightId; - this.voiceNote = voiceNote; - this.width = width; - this.height = height; - this.quote = quote; - this.caption = caption; - this.url = url; + this.voiceNote = voiceNote; + this.width = width; + this.height = height; + this.quote = quote; + this.caption = caption; + this.url = url; + this.audioDurationMs = audioDurationMs; } @Nullable @@ -51,7 +61,9 @@ public Attachment(@NonNull String contentType, int transferState, long size, Str @Nullable public abstract Uri getThumbnailUri(); - public int getTransferState() { return transferState; } + public int getTransferState() { + return transferState; + } public boolean isInProgress() { return transferState == AttachmentState.DOWNLOADING.getValue(); @@ -65,37 +77,75 @@ public boolean isFailed() { return transferState == AttachmentState.FAILED.getValue(); } - public long getSize() { return size; } + public long getSize() { + return size; + } - public String getFilename() { return filename; } + public String getFilename() { + return filename; + } @NonNull - public String getContentType() { return contentType; } + public String getContentType() { + return contentType; + } @Nullable - public String getLocation() { return location; } + public String getLocation() { + return location; + } @Nullable - public String getKey() { return key; } + public String getKey() { + return key; + } @Nullable - public String getRelay() { return relay; } + public String getRelay() { + return relay; + } @Nullable - public byte[] getDigest() { return digest; } + public byte[] getDigest() { + return digest; + } @Nullable - public String getFastPreflightId() { return fastPreflightId; } + public String getFastPreflightId() { + return fastPreflightId; + } + + public boolean isVoiceNote() { + return voiceNote; + } - public boolean isVoiceNote() { return voiceNote; } + public int getWidth() { + return width; + } - public int getWidth() { return width; } + public int getHeight() { + return height; + } - public int getHeight() { return height; } + public boolean isQuote() { + return quote; + } - public boolean isQuote() { return quote; } + public @Nullable String getCaption() { + return caption; + } - public @Nullable String getCaption() { return caption; } + public String getUrl() { + return url; + } - public String getUrl() { return url; } + /** + * Returns the duration of the audio in milliseconds. + * This is only relevant for audio attachments. + * + * Returns -1 if the information is not available. + */ + public long getAudioDurationMs() { + return audioDurationMs; + } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.java b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.java index 8f8abfc169..de6de160a5 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.java +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/DatabaseAttachment.java @@ -1,7 +1,9 @@ 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 { @@ -18,9 +20,9 @@ public DatabaseAttachment(AttachmentId attachmentId, long mmsId, 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 + String url, long audioDurationMs ) { - super(contentType, transferProgress, size, filename, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, caption, url); + super(contentType, transferProgress, size, filename, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, caption, url, audioDurationMs); this.attachmentId = attachmentId; this.hasData = hasData; this.hasThumbnail = hasThumbnail; diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java index 662795ddf0..8d0d5b50e0 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java @@ -7,7 +7,6 @@ import org.session.libsignal.utilities.guava.Optional; import org.session.libsignal.messages.SignalServiceAttachment; -import org.session.libsignal.messages.SignalServiceDataMessage; import org.session.libsignal.utilities.Base64; import org.session.libsignal.protos.SignalServiceProtos; @@ -22,7 +21,7 @@ private PointerAttachment(@NonNull String contentType, int transferState, long s @Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote, int width, int height, @Nullable String caption, String url) { - super(contentType, transferState, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, caption, url); + super(contentType, transferState, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, caption, url, -1L); } @Nullable @@ -54,22 +53,6 @@ public static List forPointers(Optional forPointersOfDataMessage(List pointers) { - List results = new LinkedList<>(); - - if (pointers != null) { - for (SignalServiceDataMessage.Quote.QuotedAttachment pointer : pointers) { - Optional result = forPointer(pointer); - - if (result.isPresent()) { - results.add(result.get()); - } - } - } - - return results; - } - public static List forPointers(List pointers) { List results = new LinkedList<>(); @@ -151,25 +134,6 @@ public static Optional forPointer(SignalServiceProtos.DataMessage.Qu thumbnail != null ? thumbnail.getUrl() : "")); } - public static Optional forPointer(SignalServiceDataMessage.Quote.QuotedAttachment pointer) { - SignalServiceAttachment thumbnail = pointer.getThumbnail(); - - return Optional.of(new PointerAttachment(pointer.getContentType(), - AttachmentState.PENDING.getValue(), - thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0, - pointer.getFileName(), - String.valueOf(thumbnail != null ? thumbnail.asPointer().getId() : 0), - thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null, - null, - thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null, - null, - false, - thumbnail != null ? thumbnail.asPointer().getWidth() : 0, - thumbnail != null ? thumbnail.asPointer().getHeight() : 0, - thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null, - thumbnail != null ? thumbnail.asPointer().getUrl() : "")); - } - /** * Converts a Session Attachment to a Signal Attachment * @param attachment Session Attachment diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/UriAttachment.java b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/UriAttachment.java index b87210faad..1aeb3f86a1 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/UriAttachment.java +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/UriAttachment.java @@ -13,15 +13,23 @@ public class UriAttachment extends Attachment { public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size, @Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption) { - this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption); + this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, -1); } public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri, @NonNull String contentType, int transferState, long size, int width, int height, @Nullable String fileName, @Nullable String fastPreflightId, - boolean voiceNote, boolean quote, @Nullable String caption) + boolean voiceNote, boolean quote, @Nullable String caption) { + this(dataUri, thumbnailUri, contentType, transferState, size, width, height, fileName, fastPreflightId, + voiceNote, quote, caption, -1); + } + + public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri, + @NonNull String contentType, int transferState, long size, int width, int height, + @Nullable String fileName, @Nullable String fastPreflightId, + boolean voiceNote, boolean quote, @Nullable String caption, long audioDurationMs) { - super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, caption, ""); + super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, caption, "", audioDurationMs); this.dataUri = dataUri; this.thumbnailUri = thumbnailUri; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java deleted file mode 100644 index 0124a3d17f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.thoughtcrime.securesms.attachments; - -import android.net.Uri; - -import androidx.annotation.Nullable; - -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState; -import org.thoughtcrime.securesms.database.MmsDatabase; - -public class MmsNotificationAttachment extends Attachment { - - public MmsNotificationAttachment(int status, long size) { - super("application/mms", getTransferStateFromStatus(status).getValue(), size, null, null, null, null, null, null, false, 0, 0, false, null, ""); - } - - @Nullable - @Override - public Uri getDataUri() { return null; } - - @Nullable - @Override - public Uri getThumbnailUri() { return null; } - - private static AttachmentState getTransferStateFromStatus(int status) { - if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED || - status == MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY) - { - return AttachmentState.PENDING; - } else if (status == MmsDatabase.Status.DOWNLOAD_CONNECTING) { - return AttachmentState.DOWNLOADING; - } else { - return AttachmentState.FAILED; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java deleted file mode 100644 index 64c7ac3df2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java +++ /dev/null @@ -1,193 +0,0 @@ -package org.thoughtcrime.securesms.audio; - -import android.media.AudioFormat; -import android.media.AudioRecord; -import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaFormat; -import android.media.MediaRecorder; - -import org.session.libsession.utilities.Util; -import org.session.libsignal.utilities.Log; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -public class AudioCodec { - - private static final String TAG = AudioCodec.class.getSimpleName(); - - private static final int SAMPLE_RATE = 44100; - private static final int SAMPLE_RATE_INDEX = 4; - private static final int CHANNELS = 1; - private static final int BIT_RATE = 32000; - - private final int bufferSize; - private final MediaCodec mediaCodec; - private final AudioRecord audioRecord; - - private boolean running = true; - private boolean finished = false; - - public AudioCodec() throws IOException { - this.bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); - this.mediaCodec = createMediaCodec(this.bufferSize); - try { - this.audioRecord = createAudioRecord(this.bufferSize); - this.mediaCodec.start(); - audioRecord.startRecording(); - } catch (Exception e) { - Log.w(TAG, e); - mediaCodec.release(); - throw new IOException(e); - } - } - - public synchronized void stop() { - running = false; - while (!finished) Util.wait(this, 0); - } - - public void start(final OutputStream outputStream) { - new Thread(new Runnable() { - @Override - public void run() { - MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - byte[] audioRecordData = new byte[bufferSize]; - ByteBuffer[] codecInputBuffers = mediaCodec.getInputBuffers(); - ByteBuffer[] codecOutputBuffers = mediaCodec.getOutputBuffers(); - - try { - while (true) { - boolean running = isRunning(); - - handleCodecInput(audioRecord, audioRecordData, mediaCodec, codecInputBuffers, running); - handleCodecOutput(mediaCodec, codecOutputBuffers, bufferInfo, outputStream); - - if (!running) break; - } - } catch (IOException e) { - Log.w(TAG, e); - } finally { - mediaCodec.stop(); - audioRecord.stop(); - - mediaCodec.release(); - audioRecord.release(); - - Util.close(outputStream); - setFinished(); - } - } - }, AudioCodec.class.getSimpleName()).start(); - } - - private synchronized boolean isRunning() { - return running; - } - - private synchronized void setFinished() { - finished = true; - notifyAll(); - } - - private void handleCodecInput(AudioRecord audioRecord, byte[] audioRecordData, - MediaCodec mediaCodec, ByteBuffer[] codecInputBuffers, - boolean running) - { - int length = audioRecord.read(audioRecordData, 0, audioRecordData.length); - int codecInputBufferIndex = mediaCodec.dequeueInputBuffer(10 * 1000); - - if (codecInputBufferIndex >= 0) { - ByteBuffer codecBuffer = codecInputBuffers[codecInputBufferIndex]; - codecBuffer.clear(); - codecBuffer.put(audioRecordData); - mediaCodec.queueInputBuffer(codecInputBufferIndex, 0, length, 0, running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM); - } - } - - private void handleCodecOutput(MediaCodec mediaCodec, - ByteBuffer[] codecOutputBuffers, - MediaCodec.BufferInfo bufferInfo, - OutputStream outputStream) - throws IOException - { - int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); - - while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { - if (codecOutputBufferIndex >= 0) { - ByteBuffer encoderOutputBuffer = codecOutputBuffers[codecOutputBufferIndex]; - - encoderOutputBuffer.position(bufferInfo.offset); - encoderOutputBuffer.limit(bufferInfo.offset + bufferInfo.size); - - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { - byte[] header = createAdtsHeader(bufferInfo.size - bufferInfo.offset); - - - outputStream.write(header); - - byte[] data = new byte[encoderOutputBuffer.remaining()]; - encoderOutputBuffer.get(data); - outputStream.write(data); - } - - encoderOutputBuffer.clear(); - - mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false); - } else if (codecOutputBufferIndex== MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { - codecOutputBuffers = mediaCodec.getOutputBuffers(); - } - - codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); - } - - } - - private byte[] createAdtsHeader(int length) { - int frameLength = length + 7; - byte[] adtsHeader = new byte[7]; - - adtsHeader[0] = (byte) 0xFF; // Sync Word - adtsHeader[1] = (byte) 0xF1; // MPEG-4, Layer (0), No CRC - adtsHeader[2] = (byte) ((MediaCodecInfo.CodecProfileLevel.AACObjectLC - 1) << 6); - adtsHeader[2] |= (((byte) SAMPLE_RATE_INDEX) << 2); - adtsHeader[2] |= (((byte) CHANNELS) >> 2); - adtsHeader[3] = (byte) (((CHANNELS & 3) << 6) | ((frameLength >> 11) & 0x03)); - adtsHeader[4] = (byte) ((frameLength >> 3) & 0xFF); - adtsHeader[5] = (byte) (((frameLength & 0x07) << 5) | 0x1f); - adtsHeader[6] = (byte) 0xFC; - - return adtsHeader; - } - - private AudioRecord createAudioRecord(int bufferSize) throws SecurityException { - return new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10); - } - - private MediaCodec createMediaCodec(int bufferSize) throws IOException { - MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); - MediaFormat mediaFormat = new MediaFormat(); - - mediaFormat.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); - mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); - mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); - mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); - mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); - - try { - mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - } catch (Exception e) { - Log.w(TAG, e); - mediaCodec.release(); - throw new IOException(e); - } - - return mediaCodec; - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java deleted file mode 100644 index 1eb663c3d6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.thoughtcrime.securesms.audio; - -import android.content.Context; -import android.net.Uri; -import android.os.ParcelFileDescriptor; -import android.util.Pair; -import androidx.annotation.NonNull; -import java.io.IOException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; - -import org.session.libsession.utilities.MediaTypes; -import org.session.libsession.utilities.Util; -import org.session.libsignal.utilities.ListenableFuture; -import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.SettableFuture; -import org.session.libsignal.utilities.ThreadUtils; -import org.thoughtcrime.securesms.providers.BlobProvider; -import org.thoughtcrime.securesms.util.MediaUtil; - -public class AudioRecorder { - - private static final String TAG = AudioRecorder.class.getSimpleName(); - - private static final ExecutorService executor = ThreadUtils.newDynamicSingleThreadedExecutor(); - - private final Context context; - - private AudioCodec audioCodec; - private Future blobWritingTask; - - // Simple interface that allows us to provide a callback method to our `startRecording` method - public interface AudioMessageRecordingFinishedCallback { - void onAudioMessageRecordingFinished(); - } - - public AudioRecorder(@NonNull Context context) { - this.context = context; - } - - public void startRecording(AudioMessageRecordingFinishedCallback callback) { - Log.i(TAG, "startRecording()"); - - executor.execute(() -> { - Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId()); - try { - if (audioCodec != null) { - Log.e(TAG, "Trying to start recording while another recording is in progress, exiting..."); - return; - } - - ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe(); - - blobWritingTask = BlobProvider.getInstance() - .forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0) - .withMimeType(MediaTypes.AUDIO_AAC) - .createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Error during recording", e)); - - audioCodec = new AudioCodec(); - audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); - - callback.onAudioMessageRecordingFinished(); - } catch (IOException e) { - Log.w(TAG, e); - } - }); - } - - public @NonNull ListenableFuture> stopRecording(boolean voiceMessageMeetsMinimumDuration) { - Log.i(TAG, "stopRecording()"); - - final SettableFuture> future = new SettableFuture<>(); - - executor.execute(() -> { - if (audioCodec == null || blobWritingTask == null) { - sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!")); - return; - } - - audioCodec.stop(); - try { - final Uri captureUri = blobWritingTask.get(); - long size = 0L; - // Only obtain the media size if the voice message was at least our minimum allowed - // duration (bypassing this work prevents the audio recording mechanism from getting into - // a broken state should the user rapidly spam the record button for several seconds). - if (voiceMessageMeetsMinimumDuration) { - size = MediaUtil.getMediaSize(context, captureUri); - } - sendToFuture(future, new Pair<>(captureUri, size)); - } catch (IOException | ExecutionException | InterruptedException e) { - Log.w(TAG, e); - sendToFuture(future, e); - } - - audioCodec = null; - blobWritingTask = null; - }); - - return future; - } - - private void sendToFuture(final SettableFuture future, final Exception exception) { - Util.runOnMain(() -> future.setException(exception)); - } - - private void sendToFuture(final SettableFuture future, final T result) { - Util.runOnMain(() -> future.set(result)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.kt b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.kt new file mode 100644 index 0000000000..91c469d027 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.kt @@ -0,0 +1,199 @@ +package org.thoughtcrime.securesms.audio + +import android.content.Context +import android.media.MediaRecorder +import android.os.Build +import android.os.SystemClock +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.session.libsignal.utilities.Log +import java.io.File +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +private const val TAG = "AudioRecorder" + +data class AudioRecordResult( + val file: File, + val length: Long, + val duration: Duration +) + + +class AudioRecorderHandle( + private val onStopCommand: suspend () -> Unit, + private val deferred: Deferred>, + private val startedResult: SharedFlow>, +) { + + private val listenerScope = CoroutineScope(Dispatchers.Main) + + /** + * Add a listener that will be called on main thread, when the recording has started. + * + * Note that after stop/cancel is called, this listener will not be called again. + */ + fun addOnStartedListener(onStartedResult: (Result) -> Unit) { + listenerScope.launch { + startedResult.collectLatest { result -> + onStartedResult(result) + } + } + } + + /** + * Stop the recording process and return the result. Note that if there's error + * during the recording, this method will throw an exception. + */ + suspend fun stop(): AudioRecordResult { + listenerScope.cancel() + onStopCommand() + return deferred.await().getOrThrow() + } + + /** + * Cancel the recording process and discard any result. + * + * The cancellation is best effort only. When the method returns, there's no + * guarantee that the recording has been stopped. But it's guaranteed that if you + * spin up a new recording immediately after calling this method, the new recording session + * won't start until the old one is properly cleaned up. + */ + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) + fun cancel() { + listenerScope.cancel() + deferred.cancel() + + if (deferred.isCompleted && deferred.getCompleted().isSuccess) { + // Clean up the temporary file if the recording was completed while we were cancelling. + GlobalScope.launch { + deferred.getCompleted().getOrThrow().file.delete() + } + } + } +} + +private sealed interface RecorderCommand { + data object Stop : RecorderCommand + data class ErrorReceived(val error: Throwable) : RecorderCommand +} + +// There can only be on instance of MediaRecorder running at a time, we use a coroutine Mutex to ensure only +// one coroutine can access the MediaRecorder at a time. +private val mediaRecorderMutex = Mutex() + +/** + * Start recording audio. THe recording will be bound to the lifecycle of the coroutine scope. + * + * To stop recording and grab the result, call [AudioRecorderHandle.stop] + */ +fun recordAudio( + scope: CoroutineScope, + context: Context, +): AudioRecorderHandle { + // Channel to send commands to the recorder coroutine. + val commandChannel = Channel(capacity = 1) + + // Channel to notify if the recording has started successfully. + val startResultChannel = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + + // Start the recording in a coroutine + val deferred = scope.async(Dispatchers.IO) { + runCatching { + mediaRecorderMutex.withLock { + val recorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MediaRecorder(context) + } else { + MediaRecorder() + } + + var started = false + + try { + val file by lazy { + File.createTempFile("audio_recording_", ".m4a", context.cacheDir) + } + + recorder.setAudioSource(MediaRecorder.AudioSource.MIC) + recorder.setAudioChannels(1) + recorder.setAudioSamplingRate(44100) + recorder.setAudioEncodingBitRate(32000) + recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + recorder.setOutputFile(file) + recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + recorder.setOnErrorListener { _, what, extra -> + commandChannel.trySend( + RecorderCommand.ErrorReceived( + RuntimeException("MediaRecorder error: what=$what, extra=$extra") + ) + ) + } + + recorder.prepare() + recorder.start() + val recordingStarted = SystemClock.elapsedRealtime() + started = true + startResultChannel.emit(Result.success(Unit)) + + // Wait for either stop signal or error + when (val c = commandChannel.receive()) { + is RecorderCommand.Stop -> { + Log.d(TAG, "Received stop command, stopping recording.") + val duration = + (SystemClock.elapsedRealtime() - recordingStarted).milliseconds + recorder.stop() + + val length = file.length() + + return@runCatching AudioRecordResult( + file = file, + length = length, + duration = duration + ) + } + + is RecorderCommand.ErrorReceived -> { + Log.e(TAG, "Error received during recording: ${c.error.message}") + file.delete() + throw c.error + } + } + } catch (e: Exception) { + if (e is CancellationException) { + Log.d(TAG, "Recording cancelled by coroutine cancellation") + } else { + Log.e(TAG, "Error during audio recording", e) + } + + if (!started) { + startResultChannel.emit(Result.failure(e)) + } + throw e + } finally { + Log.d(TAG, "Releasing MediaRecorder resources") + recorder.release() + } + } + } + } + + return AudioRecorderHandle( + onStopCommand = { commandChannel.send(RecorderCommand.Stop) }, + deferred = deferred, + startedResult = startResultChannel + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 58f08aff08..a529f61dcf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -59,6 +59,7 @@ import com.annimon.stream.Stream import com.bumptech.glide.Glide import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow @@ -115,7 +116,8 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.FullComposeActivity.Companion.applyCommonPropertiesForCompose import org.thoughtcrime.securesms.ScreenLockActionBarActivity import org.thoughtcrime.securesms.attachments.ScreenshotObserver -import org.thoughtcrime.securesms.audio.AudioRecorder +import org.thoughtcrime.securesms.audio.AudioRecorderHandle +import org.thoughtcrime.securesms.audio.recordAudio import org.thoughtcrime.securesms.components.TypingStatusSender import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity @@ -150,6 +152,7 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase @@ -210,6 +213,7 @@ import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.webrtc.WebRtcCallActivity import org.thoughtcrime.securesms.webrtc.WebRtcCallActivity.Companion.ACTION_START_CALL import org.thoughtcrime.securesms.webrtc.WebRtcCallBridge.Companion.EXTRA_RECIPIENT_ADDRESS +import java.io.File import java.lang.ref.WeakReference import java.util.LinkedList import java.util.Locale @@ -258,6 +262,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, @Inject lateinit var typingStatusRepository: TypingStatusRepository @Inject lateinit var typingStatusSender: TypingStatusSender @Inject lateinit var openGroupManager: OpenGroupManager + @Inject lateinit var attachmentDatabase: AttachmentDatabase override val applyDefaultWindowInsets: Boolean get() = false @@ -309,7 +314,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, private var unreadCount = Int.MAX_VALUE // Attachments private var voiceMessageStartTimestamp: Long = 0L - private val audioRecorder = AudioRecorder(this) + private var audioRecorderHandle: AudioRecorderHandle? = null private val stopAudioHandler = Handler(Looper.getMainLooper()) private val stopVoiceMessageRecordingTask = Runnable { sendVoiceMessage() } private val attachmentManager by lazy { AttachmentManager(this, this) } @@ -2024,7 +2029,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, outgoingTextMessage, false, message.sentTimestamp!!, - null, true ), false) @@ -2040,7 +2044,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, attachments: List, body: String?, quotedMessage: MessageRecord? = binding.inputBar.quote, - linkPreview: LinkPreview? = null + linkPreview: LinkPreview? = null, + deleteAttachmentFilesAfterSave: Boolean = false, ): Pair? { if (viewModel.recipient == null) { Log.w(TAG, "Cannot send attachments to a null recipient") @@ -2091,9 +2096,16 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, outgoingTextMessage, viewModel.threadId, false, - null, runThreadUpdate = true - ), true) + ), mms = true) + + if (deleteAttachmentFilesAfterSave) { + attachments + .asSequence() + .mapNotNull { a -> a.dataUri?.takeIf { it.scheme == "file" }?.path?.let(::File) } + .filter { it.exists() } + .forEach { it.delete() } + } waitForApprovalJobToBeSubmitted() @@ -2225,26 +2237,33 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, binding.inputBar.voiceRecorderState = VoiceRecorderState.SettingUpToRecord if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) { - showVoiceMessageUI() - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - - // Allow the caller (us!) to define what should happen when the voice recording finishes. - // Specifically in this instance, if we just tap the record audio button then by the time - // we actually finish setting up and get here the recording has been cancelled and the voice - // recorder state is Idle! As such we'll only tick the recorder state over to Recording if - // we were still in the SettingUpToRecord state when we got here (i.e., the record voice - // message button is still held or is locked to keep recording audio without being held). - val callback: () -> Unit = { - if (binding.inputBar.voiceRecorderState == VoiceRecorderState.SettingUpToRecord) { - binding.inputBar.voiceRecorderState = VoiceRecorderState.Recording - } - } + // Cancel any previous recording attempt + audioRecorderHandle?.cancel() + audioRecorderHandle = null + + audioRecorderHandle = recordAudio(lifecycleScope, this@ConversationActivityV2).also { + it.addOnStartedListener { result -> + if (result.isSuccess) { + showVoiceMessageUI() + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - voiceMessageStartTimestamp = System.currentTimeMillis() - audioRecorder.startRecording(callback) + voiceMessageStartTimestamp = System.currentTimeMillis() + binding.inputBar.voiceRecorderState = VoiceRecorderState.Recording - // Limit voice messages to 5 minute each - stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 5.minutes.inWholeMilliseconds) + // Limit voice messages to 5 minute each + stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 5.minutes.inWholeMilliseconds) + } else { + Log.e(TAG, "Error while starting voice message recording", result.exceptionOrNull()) + hideVoiceMessageUI() + binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle + Toast.makeText( + this@ConversationActivityV2, + R.string.audioUnableToRecord, + Toast.LENGTH_LONG + ).show() + } + } + } } else { binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle @@ -2257,82 +2276,80 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } } - override fun sendVoiceMessage() { - Log.i(TAG, "Sending voice message at: ${System.currentTimeMillis()}") - + private fun stopRecording(send: Boolean) { // When the record voice message button is released we always need to reset the UI and cancel // any further recording operation. hideVoiceMessageUI() window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - // How long was the voice message? Because the pointer up event could have been a regular - // hold-and-release or a release over the lock icon followed by a final tap to send so we - // update the voice message duration based on the current time here. - val voiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartTimestamp + binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle - val voiceMessageMeetsMinimumDuration = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) - val future = audioRecorder.stopRecording(voiceMessageMeetsMinimumDuration) + // Clear the audio session immediately for the next recording attempt + val handle = audioRecorderHandle + audioRecorderHandle = null stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) - binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle - - // Generate a filename from the current time such as: "Session-VoiceMessage_2025-01-08-152733.aac" - val voiceMessageFilename = FilenameUtils.constructNewVoiceMessageFilename(applicationContext) + if (handle == null) { + Log.w(TAG, "Audio recorder handle is null - cannot stop recording") + return + } - // Voice message too short? Warn with toast instead of sending. - // Note: The 0L check prevents the warning toast being shown when leaving the conversation activity. - if (voiceMessageDurationMS != 0L && !voiceMessageMeetsMinimumDuration) { + if (!MediaUtil.voiceMessageMeetsMinimumDuration(System.currentTimeMillis() - voiceMessageStartTimestamp)) { + handle.cancel() + // If the voice message is too short, we show a toast and return early + Log.w(TAG, "Voice message is too short: ${System.currentTimeMillis() - voiceMessageStartTimestamp}ms") voiceNoteTooShortToast.setText(applicationContext.getString(R.string.messageVoiceErrorShort)) showVoiceMessageToastIfNotAlreadyVisible() return } - // Note: We could return here if there was a network or node path issue, but instead we'll try - // our best to send the voice message even if it might fail - because in that case it'll get put - // into the draft database and can be retried when we regain network connectivity and a working - // node path. + // If we don't send, we'll cancel the audio recording + if (!send) { + handle.cancel() + return + } - // Attempt to send it the voice message - future.addListener(object : ListenableFuture.Listener> { + // If we do send, we will stop the audio recording, wait for it to complete successfully, + // then send the audio message as an attachment. + lifecycleScope.launch { + try { + val result = handle.stop() - override fun onSuccess(result: Pair) { - val uri = result.first - val dataSizeBytes = result.second + // Generate a filename from the current time such as: "Session-VoiceMessage_2025-01-08-152733.aac" + val voiceMessageFilename = FilenameUtils.constructNewVoiceMessageFilename(applicationContext) - // Only proceed with sending the voice message if it's long enough - if (voiceMessageMeetsMinimumDuration) { - val formattedAudioDuration = MediaUtil.getFormattedVoiceMessageDuration(voiceMessageDurationMS) - val audioSlide = AudioSlide(this@ConversationActivityV2, uri, voiceMessageFilename, dataSizeBytes, MediaTypes.AUDIO_AAC, true, formattedAudioDuration) - val slideDeck = SlideDeck() - slideDeck.addSlide(audioSlide) - sendAttachments(slideDeck.asAttachments(), body = null) - } - } + val audioSlide = AudioSlide(this@ConversationActivityV2, + Uri.fromFile(result.file), + voiceMessageFilename, + result.length, + MediaTypes.AUDIO_AAC, + true, + result.duration.inWholeMilliseconds) - override fun onFailure(e: ExecutionException) { + val slideDeck = SlideDeck() + slideDeck.addSlide(audioSlide) + sendAttachments(slideDeck.asAttachments(), body = null, deleteAttachmentFilesAfterSave = true) + + } catch (ec: CancellationException) { + // If we get cancelled then do nothing + throw ec + } catch (ec: Exception) { + Log.e(TAG, "Error while recording", ec) Toast.makeText(this@ConversationActivityV2, R.string.audioUnableToRecord, Toast.LENGTH_LONG).show() } - }) + } + } + + override fun sendVoiceMessage() { + Log.i(TAG, "Sending voice message at: ${System.currentTimeMillis()}") + + stopRecording(true) } // Cancel voice message is called when the user is press-and-hold recording a voice message and then // slides the microphone icon left, or when they lock voice recording on but then later click Cancel. override fun cancelVoiceMessage() { - val voiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartTimestamp - - hideVoiceMessageUI() - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - val voiceMessageMeetsMinimumDuration = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) - audioRecorder.stopRecording(voiceMessageMeetsMinimumDuration) - stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) - - binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle - - // Note: The 0L check prevents the warning toast being shown when leaving the conversation activity - if (voiceMessageDurationMS != 0L && !voiceMessageMeetsMinimumDuration) { - voiceNoteTooShortToast.setText(applicationContext.getString(R.string.messageVoiceErrorShort)) - showVoiceMessageToastIfNotAlreadyVisible() - } + stopRecording(false) } override fun selectMessages(messages: Set) { 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 f77d11a277..271ce60893 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 @@ -56,11 +56,16 @@ class VoiceMessageView @JvmOverloads constructor( cornerMask.setBottomRightRadius(cornerRadii[2]) cornerMask.setBottomLeftRadius(cornerRadii[3]) - // In the case of transmitting a voice message we extract and set the interim upload duration from the audio slide's `caption` field. - // Note: The UriAttachment `caption` field was previously always null for AudioSlides, so there is no harm in re-using it in this way. - // In the case of uploaded audio files we do not have a duration until file processing is complete, in which case we set a reasonable - // placeholder value while we determine the duration of the uploaded audio. - binding.voiceMessageViewDurationTextView.text = if (audioSlide.caption.isPresent) audioSlide.caption.get().toString() else "--:--" + // This sets the final duration of the uploaded voice message + (audioSlide.asAttachment() as? DatabaseAttachment)?.let { attachment -> + if (attachment.audioDurationMs > 0) { + val formattedVoiceMessageDuration = MediaUtil.getFormattedVoiceMessageDuration(attachment.audioDurationMs) + binding.voiceMessageViewDurationTextView.text = formattedVoiceMessageDuration + } else { + Log.w(TAG, "For some reason attachment.audioDurationMs was NOT greater than zero!") + binding.voiceMessageViewDurationTextView.text = "--:--" + } + } // On initial upload (and while processing audio) we will exit at this point and then return when processing is complete if (audioSlide.isPendingDownload || audioSlide.isInProgress) { @@ -69,29 +74,13 @@ class VoiceMessageView @JvmOverloads constructor( } this.player = AudioSlidePlayer.createFor(context.applicationContext, audioSlide, this) + } - // This sets the final duration of the uploaded voice message - (audioSlide.asAttachment() as? DatabaseAttachment)?.let { attachment -> - attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras -> - - // When audio processing is complete we set the final audio duration. For recorded voice - // messages this will be identical to our interim duration, but for uploaded audio files - // it will update the placeholder to the actual audio duration now that we know it. - if (audioExtras.durationMs > 0) { - durationMS = audioExtras.durationMs - val formattedVoiceMessageDuration = MediaUtil.getFormattedVoiceMessageDuration(durationMS) - binding.voiceMessageViewDurationTextView.text = formattedVoiceMessageDuration - } else { - Log.w(TAG, "For some reason audioExtras.durationMs was NOT greater than zero!") - binding.voiceMessageViewDurationTextView.text = "--:--" - } - - binding.voiceMessageViewDurationTextView.visibility = VISIBLE - } - } + override fun onPlayerStart(player: AudioSlidePlayer) { + isPlaying = true + durationMS = player.duration } - override fun onPlayerStart(player: AudioSlidePlayer) { isPlaying = true } override fun onPlayerStop(player: AudioSlidePlayer) { isPlaying = false } override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) { @@ -109,6 +98,7 @@ class VoiceMessageView @JvmOverloads constructor( // As playback progress increases the remaining duration of the audio decreases val remainingDurationMS = durationMS - (progress * durationMS.toDouble()).roundToLong() + binding.voiceMessageViewDurationTextView.text = MediaUtil.getFormattedVoiceMessageDuration(remainingDurationMS) val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java index d616d2c236..6c15b68c5f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java @@ -394,10 +394,7 @@ public enum MediaType { switch (this) { case IMAGE: return new ImageSlide(context, uri, extractedFilename, dataSize, width, height, null); - - // Note: If we come through this path we will not yet have an AudioSlide duration so we set an interim placeholder value. - case AUDIO: return new AudioSlide(context, uri, extractedFilename, dataSize, false, "--:--"); - + case AUDIO: return new AudioSlide(context, uri, extractedFilename, dataSize, false, -1L); case VIDEO: return new VideoSlide(context, uri, extractedFilename, dataSize); case VCARD: case DOCUMENT: return new DocumentSlide(context, uri, extractedFilename, mimeType, dataSize); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 63a13ddb53..5610eb566d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -131,9 +131,10 @@ public class AttachmentDatabase extends Database { 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}; + CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID, URL, + AUDIO_DURATION}; - private static final String[] PROJECTION_AUDIO_EXTRAS = new String[] {AUDIO_VISUAL_SAMPLES, AUDIO_DURATION}; + private static final String[] PROJECTION_AUDIO_EXTRAS = new String[] {AUDIO_VISUAL_SAMPLES}; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " + @@ -197,17 +198,6 @@ public AttachmentDatabase(Context context, Provider databas } } - public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) - throws MmsException - { - SQLiteDatabase database = getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, AttachmentState.FAILED.getValue()); - - 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 = getReadableDatabase(); @@ -255,23 +245,6 @@ public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) } } - public @NonNull List getPendingAttachments() { - final SQLiteDatabase database = getReadableDatabase(); - final List attachments = new LinkedList<>(); - - Cursor cursor = null; - try { - cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(AttachmentState.DOWNLOADING.getValue())}, null, null, null); - while (cursor != null && cursor.moveToNext()) { - attachments.addAll(getAttachment(cursor)); - } - } finally { - if (cursor != null) cursor.close(); - } - - return attachments; - } - public @NonNull List getAllAttachments() { SQLiteDatabase database = getReadableDatabase(); Cursor cursor = null; @@ -391,21 +364,6 @@ public void deleteAttachment(@NonNull AttachmentId id) { } } - @SuppressWarnings("ResultOfMethodCallIgnored") - void deleteAllAttachments() { - SQLiteDatabase database = 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())) { @@ -528,30 +486,6 @@ public void handleFailedAttachmentUpload(@NonNull AttachmentId id) { 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 @@ -604,7 +538,8 @@ public void handleFailedAttachmentUpload(@NonNull AttachmentId id) { mediaStream.getHeight(), databaseAttachment.isQuote(), databaseAttachment.getCaption(), - databaseAttachment.getUrl()); + databaseAttachment.getUrl(), + databaseAttachment.getAudioDurationMs()); } public void markAttachmentUploaded(long messageId, Attachment attachment) { @@ -752,13 +687,15 @@ public List getAttachment(@NonNull Cursor cursor) { object.getInt(HEIGHT), object.getInt(QUOTE) == 1, object.getString(CAPTION), - "")); // TODO: Not sure if this will break something + "", // TODO: Not sure if this will break something + object.getLong(AUDIO_DURATION))); } } return new ArrayList<>(result); } else { int urlIndex = cursor.getColumnIndex(URL); + int audioDurationIndex = cursor.getColumnIndexOrThrow(AUDIO_DURATION); 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)), @@ -778,7 +715,9 @@ public List getAttachment(@NonNull Cursor cursor) { cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)), cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1, cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)), - urlIndex > 0 ? cursor.getString(urlIndex) : "")); + urlIndex > 0 ? cursor.getString(urlIndex) : "", + cursor.isNull(audioDurationIndex) ? -1L : cursor.getLong(audioDurationIndex)) + ); } } catch (JSONException e) { throw new AssertionError(e); @@ -818,6 +757,10 @@ private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean contentValues.put(QUOTE, quote); contentValues.put(CAPTION, attachment.getCaption()); contentValues.put(URL, attachment.getUrl()); + long audioDuration = attachment.getAudioDurationMs(); + if (audioDuration > 0) { + contentValues.put(AUDIO_DURATION, audioDuration); + } if (dataInfo != null) { contentValues.put(DATA, dataInfo.file.getAbsolutePath()); @@ -944,15 +887,6 @@ public boolean setAttachmentAudioExtras(@NonNull DatabaseAttachmentAudioExtras e 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java index 960e5b1da7..118b1cc9d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -43,6 +43,7 @@ public class MediaDatabase extends Database { + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.AUDIO_DURATION + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", " 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 e67e6ba04f..f52070a90f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -51,7 +51,6 @@ import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils.queue import org.session.libsignal.utilities.Util.SECURE_RANDOM import org.session.libsignal.utilities.guava.Optional -import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MessageId @@ -647,7 +646,6 @@ class MmsDatabase(context: Context, databaseHelper: Provider { if (threadId < 0 ) throw MmsException("No thread ID supplied!") if (retrieved.isExpirationUpdate) deleteExpirationTimerMessages(threadId, true.takeUnless { retrieved.isGroup }) - val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate) + val messageId = insertMessageOutbox( + retrieved, + threadId, + false, + serverTimestamp, + runThreadUpdate + ) if (messageId == -1L) { Log.w(TAG, "insertSecureDecryptedMessageOutbox believes the MmsDatabase insertion failed.") return Optional.absent() @@ -708,7 +712,6 @@ class MmsDatabase(context: Context, databaseHelper: Provider, linkPreviews: List, contentValues: ContentValues, - insertListener: InsertListener?, ): Long { val db = writableDatabase val partsDatabase = get(context).attachmentDatabase() @@ -870,7 +871,6 @@ class MmsDatabase(context: Context, databaseHelper: Provider insertMessageOutbox(long threadId, OutgoingTextMes if (threadId == -1) { threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(message.getRecipient()); } - long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null, runThreadUpdate); + long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, runThreadUpdate); if (messageId == -1) { return Optional.absent(); } @@ -563,7 +563,7 @@ public Optional insertMessageOutbox(long threadId, OutgoingTextMes } public long insertMessageOutbox(long threadId, OutgoingTextMessage message, - boolean forceSms, long date, InsertListener insertListener, + boolean forceSms, long date, boolean runThreadUpdate) { long type = Types.BASE_SENDING_TYPE; @@ -597,9 +597,6 @@ public long insertMessageOutbox(long threadId, OutgoingTextMessage message, SQLiteDatabase db = getWritableDatabase(); long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); - if (insertListener != null) { - insertListener.onComplete(); - } if (runThreadUpdate) { DatabaseComponent.get(context).threadDatabase().update(threadId, true); @@ -890,8 +887,4 @@ public void close() { } } - public interface InsertListener { - public void onComplete(); - } - } 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 d25d0cfdd2..cf2da74f53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -254,12 +254,6 @@ open class Storage @Inject constructor( return registrationID } - override fun persistAttachments(messageID: Long, attachments: List): List { - val database = attachmentDatabase - val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() } - return database.insertAttachments(messageID, databaseAttachments) - } - override fun getAttachmentsForMessage(mmsMessageId: Long): List { return attachmentDatabase.getAttachmentsForMessage(mmsMessageId) } @@ -872,7 +866,12 @@ open class Storage @Inject constructor( Log.w(TAG, "Bailing from insertOutgoingInfoMessage because we believe the message has already been sent!") return null } - val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true) + val infoMessageID = mmsDB.insertMessageOutbox( + infoMessage, + threadID, + false, + runThreadUpdate = true + ) mmsDB.markAsSent(infoMessageID, true) return infoMessageID } @@ -1030,7 +1029,12 @@ open class Storage @Inject constructor( val mmsSmsDB = mmsSmsDatabase // check for conflict here, not returning duplicate in case it's different if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return null - val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true) + val infoMessageID = mmsDB.insertMessageOutbox( + infoMessage, + threadID, + false, + runThreadUpdate = true + ) mmsDB.markAsSent(infoMessageID, true) return infoMessageID } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.kt index 47fe00603e..23e89efa38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.kt @@ -35,7 +35,7 @@ class AudioSlide : Slide { override val thumbnailUri: Uri? get() = null - constructor(context: Context, uri: Uri, filename: String?, dataSize: Long, voiceNote: Boolean, duration: String) + constructor(context: Context, uri: Uri, filename: String?, dataSize: Long, voiceNote: Boolean, durationMills: Long) // Note: The `caption` field of `constructAttachmentFromUri` is repurposed to store the interim : super(context, constructAttachmentFromUri( @@ -47,12 +47,13 @@ class AudioSlide : Slide { 0, // height false, // hasThumbnail filename, - duration, // AudioSlides do not have captions, so we are re-purposing this field (in AudioSlides only) to store the interim audio duration displayed during upload. + null, voiceNote, - false) // quote + false, + durationMills) // quote ) - constructor(context: Context, uri: Uri, filename: String?, dataSize: Long, contentType: String, voiceNote: Boolean, duration: String = "--:--") + constructor(context: Context, uri: Uri, filename: String?, dataSize: Long, contentType: String, voiceNote: Boolean, durationMills: Long) : super(context, UriAttachment( uri, @@ -66,7 +67,8 @@ class AudioSlide : Slide { null, // fastPreflightId voiceNote, false, // quote - duration) // AudioSlides do not have captions, so we are re-purposing this field (in AudioSlides only) to store the interim audio duration displayed during upload. + null, + durationMills) ) constructor(context: Context, attachment: Attachment) : super(context, attachment) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt index 090663fdf9..5361b10e5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt @@ -147,6 +147,7 @@ abstract class Slide(@JvmField protected val context: Context, protected val att companion object { @JvmStatic + @JvmOverloads protected fun constructAttachmentFromUri( context: Context, uri: Uri, @@ -158,7 +159,8 @@ abstract class Slide(@JvmField protected val context: Context, protected val att fileName: String?, caption: String?, voiceNote: Boolean, - quote: Boolean + quote: Boolean, + audioDurationMills: Long = -1L, ): Attachment { val resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime) val fastPreflightId = SECURE_RANDOM.nextLong().toString() @@ -175,7 +177,8 @@ abstract class Slide(@JvmField protected val context: Context, protected val att fastPreflightId, voiceNote, quote, - caption + caption, + audioDurationMills ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index 31a368687c..b7051357e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -97,14 +97,14 @@ protected Void doInBackground(Void... params) { Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message"); OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null, expiresInMillis, 0); try { - DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, null, true); + DatabaseComponent.get(context).mmsDatabase().insertMessageOutbox(reply, replyThreadId, false, true); } catch (MmsException e) { Log.w(TAG, e); } } else { Log.w("AndroidAutoReplyReceiver", "Sending regular message "); OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient, expiresInMillis, expireStartedAt); - DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, SnodeAPI.getNowWithOffset(), null, true); + DatabaseComponent.get(context).smsDatabase().insertMessageOutbox(replyThreadId, reply, false, SnodeAPI.getNowWithOffset(), true); } List messageIds = DatabaseComponent.get(context).threadDatabase().setRead(replyThreadId, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 99798a7e0d..3144bd33f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -110,7 +110,7 @@ protected Void doInBackground(Void... params) { case GroupMessage: { OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null, expiresInMillis, 0); try { - message.setId(new MessageId(mmsDatabase.insertMessageOutbox(reply, threadId, false, null, true), true)); + message.setId(new MessageId(mmsDatabase.insertMessageOutbox(reply, threadId, false, true), true)); MessageSender.send(message, address); } catch (MmsException e) { Log.w(TAG, e); @@ -119,7 +119,7 @@ protected Void doInBackground(Void... params) { } case SecureMessage: { OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient, expiresInMillis, expireStartedAt); - message.setId(new MessageId(smsDatabase.insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null, true), false)); + message.setId(new MessageId(smsDatabase.insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), true), false)); MessageSender.send(message, address); break; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java index 8d05abd81e..8270b32c34 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java @@ -4,18 +4,19 @@ import android.content.Context; import android.content.UriMatcher; import android.net.Uri; + import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import org.session.libsession.utilities.Util; +import org.session.libsession.utilities.concurrent.SignalExecutors; +import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; -import org.session.libsignal.utilities.Log; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.concurrent.SignalExecutors; import java.io.ByteArrayInputStream; import java.io.File; @@ -26,10 +27,6 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; - -import kotlin.Pair; -import kotlin.Result; /** * Allows for the creation and retrieval of blobs. @@ -177,6 +174,7 @@ public static boolean isAuthority(@NonNull Uri uri) { return URI_MATCHER.match(uri) == MATCH; } + @WorkerThread @NonNull private static CompletableFuture writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec, @Nullable ErrorListener errorListener) throws IOException { @@ -262,6 +260,7 @@ protected BlobSpec buildBlobSpec(@NonNull StorageType storageType) { return new BlobSpec(data, id, storageType, mimeType, fileName, fileSize); } + /** * Create a blob that will exist for a single app session. An app session is defined as the * period from one {@link Application#onCreate()} to the next. diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt index 47a54bc60d..de3636f502 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/SignalAudioManager.kt @@ -54,9 +54,9 @@ class SignalAudioManager(private val context: Context, private var audioDevices: MutableSet = mutableSetOf() - private val soundPool: SoundPool = androidAudioManager.createSoundPool() - private val connectedSoundId = soundPool.load(context, R.raw.webrtc_completed, 1) - private val disconnectedSoundId = soundPool.load(context, R.raw.webrtc_disconnected, 1) + private val soundPool: SoundPool by lazy { androidAudioManager.createSoundPool() } + private val connectedSoundId by lazy { soundPool.load(context, R.raw.webrtc_completed, 1) } + private val disconnectedSoundId by lazy { soundPool.load(context, R.raw.webrtc_disconnected, 1) } private val incomingRinger = IncomingRinger(context) private val outgoingRinger = OutgoingRinger(context) diff --git a/app/src/main/res/layout/view_voice_message.xml b/app/src/main/res/layout/view_voice_message.xml index 666c3febce..cf2416322e 100644 --- a/app/src/main/res/layout/view_voice_message.xml +++ b/app/src/main/res/layout/view_voice_message.xml @@ -1,7 +1,7 @@ -