Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SES-1486] Short voice message fix #1523

Merged
merged 31 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3cdd383
Initial working push with debug comments
alansley Jun 25, 2024
def1265
Fixes #1522
alansley Jun 25, 2024
c15a8ee
Cleanup, prevent multi-pointer recording, and don't show short msg to…
alansley Jun 25, 2024
d9e1f73
Adjusted comment phrasing
alansley Jun 25, 2024
d5339b7
Fix comment phrasing
alansley Jun 25, 2024
6bb760f
Fixed inadvertant short voice message toast on exit conversation acti…
alansley Jun 25, 2024
9e6c6af
Comment adjustment
alansley Jun 25, 2024
753b49c
Comment phrasing
alansley Jun 25, 2024
939694d
Adjusted AudioRecorder.startRecording to take a callback function rat…
alansley Jun 25, 2024
bd3655b
Performed Thomas' PR feedback
alansley Jun 25, 2024
d8799c9
Move comment to more relevant place
alansley Jun 25, 2024
5d8fe27
Removed unused / leftover callback definition
alansley Jun 25, 2024
5858688
Removed all redundant null checks after asserting binding is not null
alansley Jun 25, 2024
880148f
Removed remaining not-null assertions & added some logged feedback to…
alansley Jun 25, 2024
bce6526
Addressed PR feedback
alansley Jun 26, 2024
e5371c5
Implemented additional PR feedback
alansley Jun 26, 2024
ae52e54
Merge branch 'dev' into SES-1486_ShortVoiceMessageFix
alansley Jun 26, 2024
1c054b8
Merge branch 'dev' into SES-1486_ShortVoiceMessageFix
AL-Session Jun 30, 2024
bc13d20
Adjusted InputBar property visibility as per PR feedback & adjusted T…
AL-Session Jun 30, 2024
6f0277b
Minor adjustment to inform user if we see an obvious network issue wh…
AL-Session Jul 1, 2024
25d6982
Merge dev
alansley Jul 2, 2024
5d9c242
Adjust comment phrasing following further testing
alansley Jul 2, 2024
f2267e9
Merge dev
alansley Jul 2, 2024
849a8f1
Added TODO comments to replace hard-coded string in toasts
alansley Jul 2, 2024
fea66bc
Addressed Thomas PR feedback suggestion
alansley Jul 3, 2024
e83b70c
Addressed another feedback suggestion
alansley Jul 3, 2024
bd9fdc7
Adjustment to continue informing user of network / node path issues
alansley Jul 3, 2024
50d3857
Improved & moved network check method
alansley Jul 3, 2024
4ecda43
Corrected ticket number into TODO comments
alansley Jul 3, 2024
05e8fd8
Addressed Andy PR feedback
alansley Jul 3, 2024
af55ae7
Adjust network connectivity checks to just log issues rather than inf…
alansley Jul 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,20 @@
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Pair;
import androidx.annotation.NonNull;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import org.session.libsession.utilities.MediaTypes;
import org.session.libsignal.utilities.Log;
import android.util.Pair;

import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;

import org.session.libsignal.utilities.ThreadUtils;
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 java.io.IOException;
import java.util.concurrent.ExecutorService;
import org.session.libsignal.utilities.ThreadUtils;
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar;
import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderState;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class AudioRecorder {
Expand All @@ -38,7 +36,7 @@ public AudioRecorder(@NonNull Context context) {
this.context = context;
}

public void startRecording() {
public void startRecording(InputBar inputBar) {
AL-Session marked this conversation as resolved.
Show resolved Hide resolved
Log.i(TAG, "startRecording()");

executor.execute(() -> {
Expand All @@ -55,9 +53,18 @@ public void startRecording() {
.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 = new AudioCodec();
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));

// 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).
if (inputBar.getVoiceRecorderState() == VoiceRecorderState.SettingUpToRecord) {
inputBar.setVoiceRecorderState(VoiceRecorderState.Recording);
}
} catch (IOException e) {
Log.w(TAG, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingView.Companion.AnimateLockDurationMS
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingView.Companion.HideDurationMS
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderState
import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidatesView
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
Expand Down Expand Up @@ -360,6 +363,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
private var recyclerScrollState: Int = RecyclerView.SCROLL_STATE_IDLE

// Lower limit for the length of voice messages - any lower and we inform the user rather than sending
private val MinimumVoiceMessageDurationMS = 1000L

// region Settings
companion object {
// Extras
Expand Down Expand Up @@ -1034,7 +1040,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun expandVoiceMessageLockView() {
val lockView = binding?.inputBarRecordingView?.lockView ?: return
val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.10f)
animation.duration = 250L
animation.duration = AnimateLockDurationMS
animation.addUpdateListener { animator ->
lockView.scaleX = animator.animatedValue as Float
lockView.scaleY = animator.animatedValue as Float
Expand All @@ -1045,7 +1051,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun collapseVoiceMessageLockView() {
val lockView = binding?.inputBarRecordingView?.lockView ?: return
val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.0f)
animation.duration = 250L
animation.duration = AnimateLockDurationMS
animation.addUpdateListener { animator ->
lockView.scaleX = animator.animatedValue as Float
lockView.scaleY = animator.animatedValue as Float
Expand All @@ -1058,7 +1064,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val slideToCancelTextView = binding?.inputBarRecordingView?.slideToCancelTextView ?: return
listOf( chevronImageView, slideToCancelTextView ).forEach { view ->
val animation = ValueAnimator.ofObject(FloatEvaluator(), view.translationX, 0.0f)
animation.duration = 250L
animation.duration = AnimateLockDurationMS
animation.addUpdateListener { animator ->
view.translationX = animator.animatedValue as Float
}
Expand All @@ -1071,7 +1077,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val inputBar = binding?.inputBar ?: return
inputBar.alpha = 1.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
animation.duration = 250L
animation.duration = HideDurationMS
animation.addUpdateListener { animator ->
inputBar.alpha = animator.animatedValue as Float
}
Expand Down Expand Up @@ -1484,16 +1490,54 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onMicrophoneButtonUp(event: MotionEvent) {
val x = event.rawX.roundToInt()
val y = event.rawY.roundToInt()
if (isValidLockViewLocation(x, y)) {
val inputBar = binding?.inputBar!!

// Lock voice recording on if the button is released over the lock area AND the
// voice recording has currently lasted for at least the time it takes to animate
// the lock area into position. Without this time check we can accidentally lock
// to recording audio on a quick tap as the lock area animates out from the record
// audio message button and the pointer-up event catches it mid-animation.
//
// Further, by limiting this to AnimateLockDurationMS rather than our minimum voice
// message length we get a fast, responsive UI that can lock 'straight away' - BUT
// we then have to artificially bump the voice message duration because if you press
// and slide to lock then release in one quick motion the pointer up event may be
// less than our minimum voice message duration - so we'll bump our recorded duration
// slightly to make sure we don't see the "Tap and hold to record..." toast when we
// finish recording the message.
if (isValidLockViewLocation(x, y) && inputBar.voiceMessageDurationMS >= AnimateLockDurationMS) {
binding?.inputBarRecordingView?.lock()
} else {
val recordButtonOverlay = binding?.inputBarRecordingView?.recordButtonOverlay ?: return
val location = IntArray(2) { 0 }
recordButtonOverlay.getLocationOnScreen(location)
val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height)
if (hitRect.contains(x, y)) {
sendVoiceMessage()
} else {

// Artificially bump message duration on lock if required
if (inputBar.voiceMessageDurationMS < MinimumVoiceMessageDurationMS) {
inputBar.voiceMessageDurationMS = MinimumVoiceMessageDurationMS;
}

// If the user put the record audio button into the lock state then we are still recording audio
binding?.inputBar?.voiceRecorderState = VoiceRecorderState.Recording
}
else // If the user didn't attempt to lock voice recording on..
{
// Regardless of where the button up event occurred we're now shutting down the recording (whether we send it or not)
binding?.inputBar?.voiceRecorderState = VoiceRecorderState.ShuttingDownAfterRecord

val rba = binding?.inputBarRecordingView?.recordButtonOverlay
if (rba != null) {
val location = IntArray(2) { 0 }
rba.getLocationOnScreen(location)
val hitRect = Rect(location[0], location[1], location[0] + rba.width, location[1] + rba.height)

// If the up event occurred over the record button overlay we send the voice message..
if (hitRect.contains(x, y)) {
sendVoiceMessage()
} else {
// ..otherwise if they've released off the button we'll cancel sending.
cancelVoiceMessage()
}
}
else
{
// Just to cover all our bases, if for whatever reason the record button overlay was null we'll also cancel recording
cancelVoiceMessage()
}
}
Expand Down Expand Up @@ -1803,7 +1847,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
showVoiceMessageUI()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.startRecording()

// Note: The input bar's recording state is set to VoiceRecorderState.Recording when it completes its setup
audioRecorder.startRecording(binding?.inputBar)

stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 300000) // Limit voice messages to 5 minute each
} else {
Permissions.with(this)
Expand All @@ -1815,10 +1862,32 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}

override fun sendVoiceMessage() {
// 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)
val future = audioRecorder.stopRecording()
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)

// ..but we'll bail without sending the voice message & inform the user that they need to press and HOLD
// the record voice message button if their message was less than 1 second long.
val inputBar = binding?.inputBar
AL-Session marked this conversation as resolved.
Show resolved Hide resolved
val voiceMessageDurationMS = inputBar?.voiceMessageDurationMS!!

// Now tear-down is complete we can move back into the idle state ready to record another voice message.
// CAREFUL: This state must be set BEFORE we show any warning toast about short messages because it early
// exits before transmitting the audio!
inputBar.voiceRecorderState = VoiceRecorderState.Idle

// 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 && voiceMessageDurationMS < MinimumVoiceMessageDurationMS) {
Toast.makeText(this@ConversationActivityV2, R.string.messageVoiceErrorShort, Toast.LENGTH_LONG).show()
inputBar.voiceMessageDurationMS = 0L
return
}

// Voice message okay? Attempt to send it.
future.addListener(object : ListenableFuture.Listener<Pair<Uri, Long>> {

override fun onSuccess(result: Pair<Uri, Long>) {
Expand All @@ -1835,10 +1904,23 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}

override fun cancelVoiceMessage() {
val inputBar = binding?.inputBar!!

hideVoiceMessageUI()
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.stopRecording()
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)

// Note: The 0L check prevents the warning toast being shown when leaving the conversation activity
val voiceMessageDuration = inputBar.voiceMessageDurationMS
if (voiceMessageDuration != 0L && voiceMessageDuration < MinimumVoiceMessageDurationMS) {
Toast.makeText(applicationContext, applicationContext.getString(R.string.messageVoiceErrorShort), Toast.LENGTH_SHORT).show()
AL-Session marked this conversation as resolved.
Show resolved Hide resolved
inputBar.voiceMessageDurationMS = 0L
}

// When tear-down is complete (via cancelling) we can move back into the idle state ready to record
// another voice message.
inputBar.voiceRecorderState = VoiceRecorderState.Idle
}

override fun selectMessages(messages: Set<MessageRecord>) {
Expand Down
Loading