Add voice recording to CFV2.

This commit is contained in:
Alex Hart
2023-06-15 09:40:08 -03:00
committed by Cody Henthorne
parent f23e5bdb44
commit ff115c2349
2 changed files with 244 additions and 5 deletions

View File

@@ -89,6 +89,7 @@ import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.audio.AudioRecorder
import org.thoughtcrime.securesms.badges.gifts.OpenableGift
import org.thoughtcrime.securesms.badges.gifts.OpenableGiftItemDecoration
import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity
@@ -112,6 +113,7 @@ import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearL
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
import org.thoughtcrime.securesms.contactshare.Contact
@@ -200,6 +202,7 @@ import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
import org.thoughtcrime.securesms.mms.AttachmentManager
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.QuoteModel
@@ -380,6 +383,7 @@ class ConversationFragment :
get() = binding.conversationSearchBottomBar.root
private lateinit var reactionDelegate: ConversationReactionDelegate
private lateinit var voiceMessageRecordingDelegate: VoiceMessageRecordingDelegate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -603,6 +607,12 @@ class ConversationFragment :
reactionDelegate = ConversationReactionDelegate(conversationReactionStub)
reactionDelegate.setOnReactionSelectedListener(OnReactionsSelectedListener())
voiceMessageRecordingDelegate = VoiceMessageRecordingDelegate(
this,
AudioRecorder(requireContext(), inputPanel),
VoiceMessageRecordingSessionCallbacks()
)
binding.conversationBanner.listener = ConversationBannerListener()
viewModel
.reminder
@@ -658,6 +668,8 @@ class ConversationFragment :
.addTo(disposables)
initializeSearch()
inputPanel.setListener(InputPanelListener())
}
private fun presentInputReadyState(inputReadyState: InputReadyState) {
@@ -2717,23 +2729,32 @@ class ConversationFragment :
}
override fun onRecorderStarted() {
// TODO [cfv2] Not yet implemented
voiceMessageRecordingDelegate.onRecorderStarted()
}
override fun onRecorderLocked() {
// TODO [cfv2] Not yet implemented
updateToggleButtonState()
voiceMessageRecordingDelegate.onRecorderLocked()
}
override fun onRecorderFinished() {
// TODO [cfv2] Not yet implemented
updateToggleButtonState()
voiceMessageRecordingDelegate.onRecorderFinished()
}
override fun onRecorderCanceled(byUser: Boolean) {
// TODO [cfv2] Not yet implemented
updateToggleButtonState()
voiceMessageRecordingDelegate.onRecorderCanceled(byUser)
}
override fun onRecorderPermissionRequired() {
// TODO [cfv2] Not yet implemented
Permissions
.with(this@ConversationFragment)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_mic_solid_24)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
.execute()
}
override fun onEmojiToggle() {
@@ -2867,4 +2888,26 @@ class ConversationFragment :
}
}
}
private inner class VoiceMessageRecordingSessionCallbacks : VoiceMessageRecordingDelegate.SessionCallback {
override fun onSessionWillBegin() {
getVoiceNoteMediaController().pausePlayback()
}
override fun sendVoiceNote(draft: VoiceNoteDraft) {
val audioSlide = AudioSlide(requireContext(), draft.uri, draft.size, MediaUtil.AUDIO_AAC, true)
sendMessageWithoutComposeInput(
slide = audioSlide
)
}
override fun cancelEphemeralVoiceNoteDraft(draft: VoiceNoteDraft) {
draftViewModel.cancelEphemeralVoiceNoteDraft(draft.asDraft())
}
override fun saveEphemeralVoiceNoteDraft(draft: VoiceNoteDraft) {
draftViewModel.cancelEphemeralVoiceNoteDraft(draft.asDraft())
}
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import android.content.pm.ActivityInfo
import android.view.WindowManager
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.core.SingleObserver
import io.reactivex.rxjava3.disposables.Disposable
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.audio.AudioRecorder
import org.thoughtcrime.securesms.audio.BluetoothVoiceNoteUtil
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
import org.thoughtcrime.securesms.conversation.VoiceRecorderWakeLock
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
/**
* Delegate class for VoiceMessage recording.
*/
class VoiceMessageRecordingDelegate(
private val fragment: Fragment,
private val audioRecorder: AudioRecorder,
private val sessionCallback: SessionCallback
) {
companion object {
private val TAG = Log.tag(VoiceMessageRecordingDelegate::class.java)
}
private val disposables = LifecycleDisposable().apply {
bindTo(fragment.viewLifecycleOwner)
}
private val voiceRecorderWakeLock = VoiceRecorderWakeLock(fragment.requireActivity())
private val bluetoothVoiceNoteUtil = BluetoothVoiceNoteUtil.create(
fragment.requireContext(),
this::beginRecording,
this::onBluetoothPermissionDenied
)
private var session: Session? = null
fun onRecorderStarted() {
val audioManager: AudioManagerCompat = ApplicationDependencies.getAndroidCallAudioManager()
if (audioManager.isBluetoothHeadsetAvailable) {
connectToBluetoothAndBeginRecording()
} else {
Log.d(TAG, "Recording from phone mic because no bluetooth devices were available.")
beginRecording()
}
}
fun onRecorderLocked() {
voiceRecorderWakeLock.acquire()
fragment.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
fun onRecorderFinished() {
bluetoothVoiceNoteUtil.disconnectBluetoothScoConnection()
voiceRecorderWakeLock.release()
vibrateAndResetOrientation(20)
session?.completeRecording()
}
fun onRecorderCanceled(byUser: Boolean) {
bluetoothVoiceNoteUtil.disconnectBluetoothScoConnection()
voiceRecorderWakeLock.release()
vibrateAndResetOrientation(50)
if (byUser) {
session?.discardRecording()
} else {
session?.saveDraft()
}
}
@Suppress("DEPRECATION")
private fun vibrateAndResetOrientation(milliseconds: Long) {
val activity = fragment.activity
if (activity != null) {
val vibrator = ServiceUtil.getVibrator(activity)
vibrator.vibrate(milliseconds)
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
private fun connectToBluetoothAndBeginRecording() {
Log.d(TAG, "Initiating Bluetooth SCO connection...")
bluetoothVoiceNoteUtil.connectBluetoothScoConnection()
}
@Suppress("DEPRECATION")
private fun beginRecording() {
val vibrator = ServiceUtil.getVibrator(fragment.requireContext())
vibrator.vibrate(20)
fragment.requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
fragment.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
sessionCallback.onSessionWillBegin()
session = Session(audioRecorder.startRecording(), sessionCallback).apply {
addTo(disposables)
}
}
private fun onBluetoothPermissionDenied() {
MaterialAlertDialogBuilder(fragment.requireContext())
.setTitle(R.string.ConversationParentFragment__bluetooth_permission_denied)
.setMessage(R.string.ConversationParentFragment__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call)
.setPositiveButton(R.string.ConversationParentFragment__open_settings) { _, _ -> fragment.startActivity(Permissions.getApplicationSettingsIntent(fragment.requireContext())) }
.setNegativeButton(R.string.ConversationParentFragment__not_now, null)
.show()
}
interface SessionCallback {
fun onSessionWillBegin()
fun sendVoiceNote(draft: VoiceNoteDraft)
fun cancelEphemeralVoiceNoteDraft(draft: VoiceNoteDraft)
fun saveEphemeralVoiceNoteDraft(draft: VoiceNoteDraft)
}
private inner class Session(
observable: Single<VoiceNoteDraft>,
private val sessionCallback: SessionCallback
) : SingleObserver<VoiceNoteDraft>, Disposable {
private var saveDraft = true
private var shouldSend = false
private var disposable = Disposable.empty()
init {
observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this)
}
override fun onSubscribe(d: Disposable) {
disposable = d
}
override fun onSuccess(draft: VoiceNoteDraft) {
when {
shouldSend -> sessionCallback.sendVoiceNote(draft)
!saveDraft -> sessionCallback.cancelEphemeralVoiceNoteDraft(draft)
else -> sessionCallback.saveEphemeralVoiceNoteDraft(draft)
}
session?.dispose()
session = null
}
override fun onError(e: Throwable) {
Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show()
Log.e(TAG, "Error in RecordingSession.", e)
session?.dispose()
session = null
}
override fun dispose() = disposable.dispose()
override fun isDisposed(): Boolean = disposable.isDisposed
fun completeRecording() {
shouldSend = true
audioRecorder.stopRecording()
}
fun discardRecording() {
saveDraft = false
shouldSend = false
audioRecorder.stopRecording()
}
fun saveDraft() {
saveDraft = true
shouldSend = false
audioRecorder.stopRecording()
}
}
}