Fix/qa 882 mic button (#943)

* WIP spam prevention

* Record voice button spam UI state confusion addressed

* Remove leftover commented code

* Unused variable removed

* Simplifying voice recording logic

* Clean up

* Clean up

* Hopefully fix toast window layout exception on microphone button spam

* Refactored voice message too short detection mechanism to avoid using deprecated call to 'someToast.view?.isShown'

---------

Co-authored-by: alansley <aclansley@gmail.com>
Co-authored-by: ThomasSession <thomas.r@getsession.org>
pull/1710/head
AL-Session 2 months ago committed by GitHub
parent 8707bb0f6f
commit e10054c4ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -93,7 +93,6 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.Stub import org.session.libsession.utilities.Stub
@ -131,8 +130,7 @@ import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton 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.InputBarDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderConstants
import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS
import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderState import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderState
import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidateAdapter import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidateAdapter
import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
@ -392,6 +390,45 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
private lateinit var reactionDelegate: ConversationReactionDelegate private lateinit var reactionDelegate: ConversationReactionDelegate
private val reactWithAnyEmojiStartPage = -1 private val reactWithAnyEmojiStartPage = -1
private val voiceNoteTooShortToast: Toast by lazy {
Toast.makeText(
applicationContext,
applicationContext.getString(R.string.messageVoiceErrorShort),
Toast.LENGTH_SHORT
).apply {
// On Android API 30 and above we can use callbacks to control our toast visible flag.
// Note: We have to do this hoop-jumping to prevent the possibility of a window layout
// crash when attempting to show a toast that is already visible should the user spam
// the microphone button, and because `someToast.view?.isShown` is deprecated.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
addCallback(object : Toast.Callback() {
override fun onToastShown() { isVoiceToastShowing = true }
override fun onToastHidden() { isVoiceToastShowing = false }
})
}
}
}
private var isVoiceToastShowing = false
// Only show a toast related to voice messages if the toast is not already showing (used if to
// rate limit & prevent toast queueing when the user spams the microphone button).
private fun showVoiceMessageToastIfNotAlreadyVisible() {
if (!isVoiceToastShowing) {
voiceNoteTooShortToast.show()
// Use a delayed callback to reset the toast visible flag after Toast.LENGTH_SHORT duration (~2000ms) ONLY on
// Android APIs < 30 which lack the onToastShown & onToastHidden callbacks.
// Note: While Toast.LENGTH_SHORT is roughly 2000ms, it is subject to change with varying Android versions or
// even between devices - we have no control over this.
// TODO: Remove the lines below and just use the callbacks when our minimum API is >= 30.
isVoiceToastShowing = true
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Handler(Looper.getMainLooper()).postDelayed( { isVoiceToastShowing = false }, 2000)
}
}
}
// Properties for what message indices are visible previously & now, as well as the scroll state // Properties for what message indices are visible previously & now, as well as the scroll state
private var previousLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION private var previousLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
@ -1086,11 +1123,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
} }
} }
private fun acceptMessageRequest() {
binding.messageRequestBar.isVisible = false
viewModel.acceptMessageRequest()
}
override fun inputBarEditTextContentChanged(newContent: CharSequence) { override fun inputBarEditTextContentChanged(newContent: CharSequence) {
val inputBarText = binding.inputBar.text // TODO check if we should be referencing newContent here instead val inputBarText = binding.inputBar.text // TODO check if we should be referencing newContent here instead
if (textSecurePreferences.isLinkPreviewsEnabled()) { if (textSecurePreferences.isLinkPreviewsEnabled()) {
@ -1134,60 +1166,62 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
override fun showVoiceMessageUI() { override fun showVoiceMessageUI() {
binding.inputBarRecordingView.show(lifecycleScope) binding.inputBarRecordingView.show(lifecycleScope)
binding.inputBar.alpha = 0.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) // Cancel any previous input bar animations and fade out the bar
animation.duration = SHOW_HIDE_VOICE_UI_DURATION_MS val inputBar = binding.inputBar
animation.addUpdateListener { animator -> inputBar.animate().cancel()
binding.inputBar.alpha = animator.animatedValue as Float inputBar.animate()
} .alpha(0f)
animation.start() .setDuration(VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS)
.start()
} }
private fun expandVoiceMessageLockView() { private fun expandVoiceMessageLockView() {
val lockView = binding.inputBarRecordingView.lockView val lockView = binding.inputBarRecordingView.lockView
val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.10f)
animation.duration = ANIMATE_LOCK_DURATION_MS lockView.animate().cancel()
animation.addUpdateListener { animator -> lockView.animate()
lockView.scaleX = animator.animatedValue as Float .scaleX(1.10f)
lockView.scaleY = animator.animatedValue as Float .scaleY(1.10f)
} .setDuration(VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS)
animation.start() .start()
} }
private fun collapseVoiceMessageLockView() { private fun collapseVoiceMessageLockView() {
val lockView = binding.inputBarRecordingView.lockView val lockView = binding.inputBarRecordingView.lockView
val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.0f)
animation.duration = ANIMATE_LOCK_DURATION_MS lockView.animate().cancel()
animation.addUpdateListener { animator -> lockView.animate()
lockView.scaleX = animator.animatedValue as Float .scaleX(1.0f)
lockView.scaleY = animator.animatedValue as Float .scaleY(1.0f)
} .setDuration(VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS)
animation.start() .start()
} }
private fun hideVoiceMessageUI() { private fun hideVoiceMessageUI() {
val chevronImageView = binding.inputBarRecordingView.chevronImageView listOf(
val slideToCancelTextView = binding.inputBarRecordingView.slideToCancelTextView binding.inputBarRecordingView.chevronImageView,
listOf( chevronImageView, slideToCancelTextView ).forEach { view -> binding.inputBarRecordingView.slideToCancelTextView
val animation = ValueAnimator.ofObject(FloatEvaluator(), view.translationX, 0.0f) ).forEach { view ->
animation.duration = ANIMATE_LOCK_DURATION_MS view.animate().cancel()
animation.addUpdateListener { animator -> view.animate()
view.translationX = animator.animatedValue as Float .translationX(0.0f)
} .setDuration(VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS)
animation.start() .start()
} }
binding.inputBarRecordingView.hide() binding.inputBarRecordingView.hide()
} }
override fun handleVoiceMessageUIHidden() { override fun handleVoiceMessageUIHidden() {
val inputBar = binding.inputBar val inputBar = binding.inputBar
inputBar.alpha = 1.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) // Cancel any previous input bar animations and fade in the bar
animation.duration = SHOW_HIDE_VOICE_UI_DURATION_MS inputBar.animate().cancel()
animation.addUpdateListener { animator -> inputBar.animate()
inputBar.alpha = animator.animatedValue as Float .alpha(1.0f)
} .setDuration(VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS)
animation.start() .start()
} }
private fun handleRecyclerViewScrolled() { private fun handleRecyclerViewScrolled() {
@ -1716,6 +1750,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
} }
override fun onMicrophoneButtonUp(event: MotionEvent) { override fun onMicrophoneButtonUp(event: MotionEvent) {
if(binding.inputBar.voiceRecorderState != VoiceRecorderState.Recording){
cancelVoiceMessage()
return
}
val x = event.rawX.roundToInt() val x = event.rawX.roundToInt()
val y = event.rawY.roundToInt() val y = event.rawY.roundToInt()
@ -1725,7 +1764,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
// to recording audio on a quick tap as the lock area animates out from the record // 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. // audio message button and the pointer-up event catches it mid-animation.
val currentVoiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartTimestamp val currentVoiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartTimestamp
if (isValidLockViewLocation(x, y) && currentVoiceMessageDurationMS >= ANIMATE_LOCK_DURATION_MS) { if (isValidLockViewLocation(x, y) && currentVoiceMessageDurationMS >= VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS) {
binding.inputBarRecordingView.lock() binding.inputBarRecordingView.lock()
// If the user put the record audio button into the lock state then we are still recording audio // If the user put the record audio button into the lock state then we are still recording audio
@ -1736,23 +1775,17 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
// If the user didn't lock voice recording on then we're stopping voice recording // If the user didn't lock voice recording on then we're stopping voice recording
binding.inputBar.voiceRecorderState = VoiceRecorderState.ShuttingDownAfterRecord binding.inputBar.voiceRecorderState = VoiceRecorderState.ShuttingDownAfterRecord
val rba = binding.inputBarRecordingView?.recordButtonOverlay val recordButtonOverlay = 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.. val location = IntArray(2) { 0 }
if (hitRect.contains(x, y)) { recordButtonOverlay.getLocationOnScreen(location)
sendVoiceMessage() val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height)
} else {
// ..otherwise if they've released off the button we'll cancel sending. // If the up event occurred over the record button overlay we send the voice message..
cancelVoiceMessage() if (hitRect.contains(x, y)) {
} sendVoiceMessage()
} } else {
else // ..otherwise if they've released off the button we'll cancel sending.
{
// Just to cover all our bases, if for whatever reason the record button overlay was null we'll also cancel recording
cancelVoiceMessage() cancelVoiceMessage()
} }
} }
@ -2067,7 +2100,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
} }
override fun startRecordingVoiceMessage() { override fun startRecordingVoiceMessage() {
Log.i(TAG, "Starting voice message recording at: ${System.currentTimeMillis()}") Log.i(TAG, "Starting voice message recording at: ${System.currentTimeMillis()} --- ${binding.inputBar.voiceRecorderState}")
binding.inputBar.voiceRecorderState = VoiceRecorderState.SettingUpToRecord
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) { if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
showVoiceMessageUI() showVoiceMessageUI()
@ -2085,13 +2119,14 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
} }
} }
binding.inputBar.voiceRecorderState = VoiceRecorderState.SettingUpToRecord
voiceMessageStartTimestamp = System.currentTimeMillis() voiceMessageStartTimestamp = System.currentTimeMillis()
audioRecorder.startRecording(callback) audioRecorder.startRecording(callback)
// Limit voice messages to 5 minute each // Limit voice messages to 5 minute each
stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 5.minutes.inWholeMilliseconds) stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 5.minutes.inWholeMilliseconds)
} else { } else {
binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO) .request(Manifest.permission.RECORD_AUDIO)
.withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired) .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired)
@ -2115,9 +2150,10 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
// update the voice message duration based on the current time here. // update the voice message duration based on the current time here.
val voiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartTimestamp val voiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartTimestamp
val voiceMessageDurationValid = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) val voiceMessageMeetsMinimumDuration = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS)
val future = audioRecorder.stopRecording(voiceMessageDurationValid) val future = audioRecorder.stopRecording(voiceMessageMeetsMinimumDuration)
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle
// Generate a filename from the current time such as: "Session-VoiceMessage_2025-01-08-152733.aac" // Generate a filename from the current time such as: "Session-VoiceMessage_2025-01-08-152733.aac"
@ -2125,9 +2161,9 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
// Voice message too short? Warn with toast instead of sending. // 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. // Note: The 0L check prevents the warning toast being shown when leaving the conversation activity.
val voiceMessageBelowMinimumDuration = !MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) if (voiceMessageDurationMS != 0L && !voiceMessageMeetsMinimumDuration) {
if (voiceMessageDurationMS != 0L && voiceMessageBelowMinimumDuration) { voiceNoteTooShortToast.setText(applicationContext.getString(R.string.messageVoiceErrorShort))
Toast.makeText(this@ConversationActivityV2, R.string.messageVoiceErrorShort, Toast.LENGTH_SHORT).show() showVoiceMessageToastIfNotAlreadyVisible()
return return
} }
@ -2144,7 +2180,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
val dataSizeBytes = result.second val dataSizeBytes = result.second
// Only proceed with sending the voice message if it's long enough // Only proceed with sending the voice message if it's long enough
if (!voiceMessageBelowMinimumDuration) { if (voiceMessageMeetsMinimumDuration) {
val formattedAudioDuration = MediaUtil.getFormattedVoiceMessageDuration(voiceMessageDurationMS) val formattedAudioDuration = MediaUtil.getFormattedVoiceMessageDuration(voiceMessageDurationMS)
val audioSlide = AudioSlide(this@ConversationActivityV2, uri, voiceMessageFilename, dataSizeBytes, MediaTypes.AUDIO_AAC, true, formattedAudioDuration) val audioSlide = AudioSlide(this@ConversationActivityV2, uri, voiceMessageFilename, dataSizeBytes, MediaTypes.AUDIO_AAC, true, formattedAudioDuration)
val slideDeck = SlideDeck() val slideDeck = SlideDeck()
@ -2169,11 +2205,13 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
val voiceMessageMeetsMinimumDuration = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) val voiceMessageMeetsMinimumDuration = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS)
audioRecorder.stopRecording(voiceMessageMeetsMinimumDuration) audioRecorder.stopRecording(voiceMessageMeetsMinimumDuration)
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle
// Note: The 0L check prevents the warning toast being shown when leaving the conversation activity // Note: The 0L check prevents the warning toast being shown when leaving the conversation activity
if (voiceMessageDurationMS != 0L && !voiceMessageMeetsMinimumDuration) { if (voiceMessageDurationMS != 0L && !voiceMessageMeetsMinimumDuration) {
Toast.makeText(applicationContext, applicationContext.getString(R.string.messageVoiceErrorShort), Toast.LENGTH_SHORT).show() voiceNoteTooShortToast.setText(applicationContext.getString(R.string.messageVoiceErrorShort))
showVoiceMessageToastIfNotAlreadyVisible()
} }
} }

@ -15,6 +15,7 @@ import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.RequestManager
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewInputBarBinding import network.loki.messenger.databinding.ViewInputBarBinding
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
@ -26,7 +27,6 @@ import org.thoughtcrime.securesms.conversation.v2.messages.QuoteView
import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.addTextChangedListener import org.thoughtcrime.securesms.util.addTextChangedListener
import org.thoughtcrime.securesms.util.contains import org.thoughtcrime.securesms.util.contains
@ -112,12 +112,14 @@ class InputBar @JvmOverloads constructor(
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
// Only start spinning up the voice recorder if we're not already recording, setting up, or tearing down // Only start spinning up the voice recorder if we're not already recording, setting up, or tearing down
if (voiceRecorderState == VoiceRecorderState.Idle) { if (voiceRecorderState == VoiceRecorderState.Idle) {
startRecordingVoiceMessage() startRecordingVoiceMessage()
} }
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
// Handle the pointer up event appropriately, whether that's to keep recording if recording was locked // Handle the pointer up event appropriately, whether that's to keep recording if recording was locked
// on, or finishing recording if just hold-to-record. // on, or finishing recording if just hold-to-record.
delegate?.onMicrophoneButtonUp(event) delegate?.onMicrophoneButtonUp(event)
@ -172,7 +174,10 @@ class InputBar @JvmOverloads constructor(
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: RequestManager) { fun draftQuote(thread: Recipient, message: MessageRecord, glide: RequestManager) {
quoteView?.let(binding.inputBarAdditionalContentContainer::removeView) quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)

@ -5,7 +5,6 @@ import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.PointF import android.graphics.PointF
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
@ -153,7 +152,7 @@ class InputBarButton : RelativeLayout {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
val newLongPressCallback = Runnable { onLongPress?.invoke() } val newLongPressCallback = Runnable { onLongPress?.invoke() }
this.longPressCallback = newLongPressCallback this.longPressCallback = newLongPressCallback
gestureHandler.postDelayed(newLongPressCallback, InputBarButton.longPressDurationThreshold) gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
onDownTimestamp = Date().time onDownTimestamp = Date().time
} }
@ -170,7 +169,7 @@ class InputBarButton : RelativeLayout {
private fun onUp(event: MotionEvent) { private fun onUp(event: MotionEvent) {
onUp?.invoke(event) onUp?.invoke(event)
collapse() collapse()
if ((Date().time - onDownTimestamp) < InputBarButton.longPressDurationThreshold) { if ((Date().time - onDownTimestamp) < longPressDurationThreshold) {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
onPress?.invoke() onPress?.invoke()
} }

@ -70,13 +70,14 @@ class InputBarRecordingView : RelativeLayout {
binding.inputBarMiddleContentContainer.alpha = 1.0f binding.inputBarMiddleContentContainer.alpha = 1.0f
binding.lockView.alpha = 1.0f binding.lockView.alpha = 1.0f
isVisible = true isVisible = true
alpha = 0.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) animate().cancel()
animation.duration = 250L animate()
animation.addUpdateListener { animator -> .alpha(1f)
alpha = animator.animatedValue as Float .setDuration(VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS)
} .withEndAction(null)
animation.start() .start()
animateDotView() animateDotView()
pulse() pulse()
animateLockViewUp() animateLockViewUp()
@ -84,18 +85,17 @@ class InputBarRecordingView : RelativeLayout {
} }
fun hide() { fun hide() {
alpha = 1.0f animate().cancel()
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) animate()
animation.duration = VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS .alpha(0f)
animation.addUpdateListener { animator -> .setDuration(VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS)
alpha = animator.animatedValue as Float .withEndAction {
if (animator.animatedFraction == 1.0f) {
isVisible = false isVisible = false
dotViewAnimation?.repeatCount = 0 dotViewAnimation?.repeatCount = 0
pulseAnimation?.removeAllUpdateListeners() pulseAnimation?.removeAllUpdateListeners()
} }
} .start()
animation.start()
delegate?.handleVoiceMessageUIHidden() delegate?.handleVoiceMessageUIHidden()
stopTimer() stopTimer()
} }
@ -110,7 +110,7 @@ class InputBarRecordingView : RelativeLayout {
val durationMS = (Date().time - startTimestamp) val durationMS = (Date().time - startTimestamp)
binding.recordingViewDurationTextView.text = MediaUtil.getFormattedVoiceMessageDuration(durationMS) binding.recordingViewDurationTextView.text = MediaUtil.getFormattedVoiceMessageDuration(durationMS)
delay(500) delay(500) // Update the voice message duration timer value every half a second
} }
} }
} }

@ -101,8 +101,6 @@ class MediaRepository {
return mediaFolders; return mediaFolders;
} }
@WorkerThread @WorkerThread
private @NonNull FolderResult getFolders(@NonNull Context context, @NonNull Uri contentUri) { private @NonNull FolderResult getFolders(@NonNull Context context, @NonNull Uri contentUri) {
Uri globalThumbnail = null; Uri globalThumbnail = null;
@ -152,7 +150,6 @@ class MediaRepository {
return new FolderResult(globalThumbnail, thumbnailTimestamp, folders); return new FolderResult(globalThumbnail, thumbnailTimestamp, folders);
} }
@WorkerThread @WorkerThread
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) { private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
List<Media> images = getMediaInBucket(context, bucketId, Images.Media.EXTERNAL_CONTENT_URI, true); List<Media> images = getMediaInBucket(context, bucketId, Images.Media.EXTERNAL_CONTENT_URI, true);
@ -165,7 +162,6 @@ class MediaRepository {
return media; return media;
} }
@WorkerThread @WorkerThread
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) { private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) {
List<Media> media = new LinkedList<>(); List<Media> media = new LinkedList<>();
@ -204,7 +200,6 @@ class MediaRepository {
return media; return media;
} }
@WorkerThread @WorkerThread
private List<Media> getPopulatedMedia(@NonNull Context context, @NonNull List<Media> media) { private List<Media> getPopulatedMedia(@NonNull Context context, @NonNull List<Media> media) {
return Stream.of(media).map(m -> { return Stream.of(media).map(m -> {
@ -260,7 +255,6 @@ class MediaRepository {
return new Media(media.getUri(), media.getFilename(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption()); return new Media(media.getUri(), media.getFilename(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption());
} }
private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException { private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
int width = media.getWidth(); int width = media.getWidth();
int height = media.getHeight(); int height = media.getHeight();
@ -364,7 +358,6 @@ class MediaRepository {
} }
} }
interface Callback<E> { interface Callback<E> {
void onComplete(@NonNull E result); void onComplete(@NonNull E result);
} }

Loading…
Cancel
Save