mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 16:49:40 +01:00
Add voice recording to CFV2.
This commit is contained in:
committed by
Cody Henthorne
parent
f23e5bdb44
commit
ff115c2349
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user