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 20 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
@@ -1,28 +1,22 @@
package org.thoughtcrime.securesms.audio;

import android.annotation.TargetApi;

import android.content.Context;
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 org.session.libsignal.utilities.ThreadUtils;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;

import java.io.IOException;
import java.util.concurrent.ExecutorService;

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class AudioRecorder {

private static final String TAG = AudioRecorder.class.getSimpleName();
Expand All @@ -34,11 +28,16 @@ public class AudioRecorder {
private AudioCodec audioCodec;
private Uri captureUri;

// 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() {
public void startRecording(AudioMessageRecordingFinishedCallback callback) {
Log.i(TAG, "startRecording()");

executor.execute(() -> {
Expand All @@ -55,9 +54,11 @@ 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]));

callback.onAudioMessageRecordingFinished();
} catch (IOException e) {
Log.w(TAG, e);
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2.input_bar

import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.graphics.PointF
Expand All @@ -10,6 +11,7 @@ import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.RelativeLayout
import android.widget.TextView
Expand All @@ -31,6 +33,15 @@ import org.thoughtcrime.securesms.util.contains
import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx

// Enums to keep track of the state of our voice recording mechanism as the user can
// manipulate the UI faster than we can setup & teardown.
enum class VoiceRecorderState {
Idle,
SettingUpToRecord,
Recording,
ShuttingDownAfterRecord
}

class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate,
TextView.OnEditorActionListener {
private lateinit var binding: ViewInputBarBinding
Expand All @@ -56,6 +67,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
get() { return binding.inputBarEditText.text?.toString() ?: "" }
set(value) { binding.inputBarEditText.setText(value) }

// Keep track of when the user pressed the record voice message button, the duration that
// they held record, and the current audio recording mechanism state.
private var voiceMessageStartMS = 0L
var voiceMessageDurationMS = 0L
var voiceRecorderState = VoiceRecorderState.Idle

val attachmentButtonsContainerHeight: Int
get() = binding.attachmentsButtonContainer.height

Expand All @@ -68,19 +85,63 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }

@SuppressLint("ClickableViewAccessibility")
private fun initialize() {
binding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true)
// Attachments button
binding.attachmentsButtonContainer.addView(attachmentsButton)
attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
attachmentsButton.onPress = { toggleAttachmentOptions() }

// Microphone button
binding.microphoneOrSendButtonContainer.addView(microphoneButton)
microphoneButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
microphoneButton.onLongPress = { startRecordingVoiceMessage() }

microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) }
microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) }
microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) }

// Use a separate 'raw' OnTouchListener to record the microphone button down/up timestamps because
// they don't get delayed by any multi-threading or delegates which throw off the timestamp accuracy.
// For example: If we bind something to `microphoneButton.onPress` and also log something in
// `microphoneButton.onUp` and tap the button then the logged output order is onUp and THEN onPress!
microphoneButton.setOnTouchListener(object : OnTouchListener {
override fun onTouch(v: View, event: MotionEvent): Boolean {

// We only handle single finger touch events so just consume the event and bail if there are more
if (event.pointerCount > 1) return true

when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Only start spinning up the voice recorder if we're not already recording, setting up, or tearing down
if (voiceRecorderState == VoiceRecorderState.Idle) {
// Take note of when we start recording so we can figure out how long the record button was held for
voiceMessageStartMS = System.currentTimeMillis()

// We are now setting up to record, and when we actually start recording then
// AudioRecorder.startRecording will move us into the Recording state.
voiceRecorderState = VoiceRecorderState.SettingUpToRecord
startRecordingVoiceMessage()
}
}
MotionEvent.ACTION_UP -> {
// Work out how long the record audio button was held for
voiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartMS;

// Regardless of our current recording state we'll always call the onMicrophoneButtonUp method
// and let the logic in that take the appropriate action as we cannot guarantee that letting
// go of the record button should always stop recording audio because the user may have moved
// the button into the 'locked' state so they don't have to keep it held down to record a voice
// message.
// Also: We need to tear down the voice recorder if it has been recording and is now stopping.
delegate?.onMicrophoneButtonUp(event)
}
}

// Return false to propagate the event rather than consuming it
return false
}
})

// Send button
binding.microphoneOrSendButtonContainer.addView(sendButton)
sendButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
Expand All @@ -90,6 +151,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
delegate?.sendMessage()
}
}

// Edit text
binding.inputBarEditText.setOnEditorActionListener(this)
if (TextSecurePreferences.isEnterSendsEnabled(context)) {
Expand Down Expand Up @@ -125,20 +187,13 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
delegate?.inputBarEditTextContentChanged(text)
}

override fun inputBarEditTextHeightChanged(newValue: Int) {
}
override fun inputBarEditTextHeightChanged(newValue: Int) { }

override fun commitInputContent(contentUri: Uri) {
delegate?.commitInputContent(contentUri)
}
override fun commitInputContent(contentUri: Uri) { delegate?.commitInputContent(contentUri) }

private fun toggleAttachmentOptions() {
delegate?.toggleAttachmentOptions()
}
private fun toggleAttachmentOptions() { delegate?.toggleAttachmentOptions() }

private fun startRecordingVoiceMessage() {
delegate?.startRecordingVoiceMessage()
}
private fun startRecordingVoiceMessage() { delegate?.startRecordingVoiceMessage() }

fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
Expand Down Expand Up @@ -227,6 +282,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
fun setSelection(index: Int) {
binding.inputBarEditText.setSelection(index)
}

// endregion
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toPx
import java.util.Date

// Constants for animation durations in milliseconds
object VoiceRecorderConstants {
const val ANIMATE_LOCK_DURATION_MS = 250L
const val DOT_ANIMATION_DURATION_MS = 500L
const val DOT_PULSE_ANIMATION_DURATION_MS = 1000L
const val SHOW_HIDE_VOICE_UI_DURATION_MS = 250L
}

class InputBarRecordingView : RelativeLayout {
private lateinit var binding: ViewInputBarRecordingBinding
private var startTimestamp = 0L
Expand Down Expand Up @@ -79,7 +87,7 @@ class InputBarRecordingView : RelativeLayout {
fun hide() {
alpha = 1.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
animation.duration = 250L
animation.duration = VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS
animation.addUpdateListener { animator ->
alpha = animator.animatedValue as Float
if (animator.animatedFraction == 1.0f) {
Expand Down Expand Up @@ -113,7 +121,7 @@ class InputBarRecordingView : RelativeLayout {
private fun animateDotView() {
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
dotViewAnimation = animation
animation.duration = 500L
animation.duration = VoiceRecorderConstants.DOT_ANIMATION_DURATION_MS
animation.addUpdateListener { animator ->
binding.dotView.alpha = animator.animatedValue as Float
}
Expand All @@ -128,7 +136,7 @@ class InputBarRecordingView : RelativeLayout {
binding.pulseView.animateSizeChange(collapsedSize, expandedSize, 1000)
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f)
pulseAnimation = animation
animation.duration = 1000L
animation.duration = VoiceRecorderConstants.DOT_PULSE_ANIMATION_DURATION_MS
animation.addUpdateListener { animator ->
binding.pulseView.alpha = animator.animatedValue as Float
if (animator.animatedFraction == 1.0f && isVisible) { pulse() }
Expand All @@ -143,7 +151,7 @@ class InputBarRecordingView : RelativeLayout {
layoutParams.bottomMargin = startMarginBottom
binding.lockView.layoutParams = layoutParams
val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom)
animation.duration = 250L
animation.duration = VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS
animation.addUpdateListener { animator ->
layoutParams.bottomMargin = animator.animatedValue as Int
binding.lockView.layoutParams = layoutParams
Expand All @@ -153,21 +161,25 @@ class InputBarRecordingView : RelativeLayout {

fun lock() {
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
fadeOutAnimation.duration = 250L
fadeOutAnimation.duration = VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS
fadeOutAnimation.addUpdateListener { animator ->
binding.inputBarMiddleContentContainer.alpha = animator.animatedValue as Float
binding.lockView.alpha = animator.animatedValue as Float
}
fadeOutAnimation.start()
val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
fadeInAnimation.duration = 250L
fadeInAnimation.duration = VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS
fadeInAnimation.addUpdateListener { animator ->
binding.inputBarCancelButton.alpha = animator.animatedValue as Float
}
fadeInAnimation.start()
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme))
binding.recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
binding.inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() }

// When the user has locked the voice recorder button on then THIS is where the next click
// is registered to actually send the voice message - it does NOT hit the microphone button
// onTouch listener again.
binding.recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() }
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/layout/view_input_bar_recording.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@

<TextView
android:id="@+id/inputBarCancelButton"
android:layout_width="100dp"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_centerInParent="true"
android:alpha="0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.recover
import org.session.libsignal.utilities.toHexString
import java.util.Date
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.set

Expand Down
2 changes: 2 additions & 0 deletions libsession/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,6 @@
<string name="ConversationItem_group_action_left">%1$s has left the group.</string>
<!-- RecipientProvider -->
<string name="RecipientProvider_unnamed_group">Unnamed group</string>

<string name="messageVoiceErrorShort">Hold to record a voice message.</string>
</resources>