diff --git a/core/data/src/main/java/com/teampatch/core/data/repository/local/LocalMemoryCardRepositoryImpl.kt b/core/data/src/main/java/com/teampatch/core/data/repository/local/LocalMemoryCardRepositoryImpl.kt index cd82e394..639f513a 100644 --- a/core/data/src/main/java/com/teampatch/core/data/repository/local/LocalMemoryCardRepositoryImpl.kt +++ b/core/data/src/main/java/com/teampatch/core/data/repository/local/LocalMemoryCardRepositoryImpl.kt @@ -13,6 +13,7 @@ import java.io.InputStream import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -29,6 +30,11 @@ internal class LocalMemoryCardRepositoryImpl @Inject constructor( ) { } + override suspend fun addAnswer(memoryCardId: String, answer: String) { + val memoryCard = memoryCardDao.getMemoryStorageById(memoryCardId.toLong()).first() + memoryCardDao.updateMemoryStorage(memoryCard.copy(content = answer)) + } + override suspend fun getQuestionMessage(memoryCardId: String): MemoryCardQuestion = FakeMemoryCardQuestion().get() override fun getMemoryCards(): Flow> { diff --git a/core/domain/src/main/java/com/teampatch/core/domain/repository/MemoryCardRepository.kt b/core/domain/src/main/java/com/teampatch/core/domain/repository/MemoryCardRepository.kt index 817edc36..0ffe4f9b 100644 --- a/core/domain/src/main/java/com/teampatch/core/domain/repository/MemoryCardRepository.kt +++ b/core/domain/src/main/java/com/teampatch/core/domain/repository/MemoryCardRepository.kt @@ -20,6 +20,11 @@ interface MemoryCardRepository { audioFile: InputStream, ) + suspend fun addAnswer( + memoryCardId: String, + answer: String, + ) + /** * 해당 추억카드 질문들을 가져오는 함수입니다. */ diff --git a/core/domain/src/main/java/com/teampatch/core/domain/usecase/memory/AddMemoryCardAnswerUseCase.kt b/core/domain/src/main/java/com/teampatch/core/domain/usecase/memory/AddMemoryCardAnswerUseCase.kt new file mode 100644 index 00000000..5230f037 --- /dev/null +++ b/core/domain/src/main/java/com/teampatch/core/domain/usecase/memory/AddMemoryCardAnswerUseCase.kt @@ -0,0 +1,13 @@ +package com.teampatch.core.domain.usecase.memory + +import com.teampatch.core.domain.repository.MemoryCardRepository +import javax.inject.Inject + +class AddMemoryCardAnswerUseCase @Inject constructor( + private val memoryCardRepository: MemoryCardRepository, +) { + + suspend operator fun invoke(memoryCardId: String, answer: String) { + memoryCardRepository.addAnswer(memoryCardId, answer) + } +} \ No newline at end of file diff --git a/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/MemoryCardRegistrationScreen.kt b/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/MemoryCardRegistrationScreen.kt index bdf4dc4e..87369f92 100644 --- a/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/MemoryCardRegistrationScreen.kt +++ b/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/MemoryCardRegistrationScreen.kt @@ -2,6 +2,12 @@ package com.teampatch.feature.memorycard.registration import android.app.Activity import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.speech.RecognitionListener +import android.speech.RecognizerIntent +import android.speech.SpeechRecognizer +import android.util.Log import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -16,6 +22,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -65,33 +75,46 @@ internal fun MemoryCardRegistrationRoute( if (!uiState.isLoading) { MemoryCardRegistrationScreen( onDismissRequest = onDismissRequest, - onRecordStartRequest = viewModel::startMemoryCardAudioRecord, - onRecordStopRequest = { - viewModel.stopMemoryCardAudioRecord() - viewModel.uploadMemoryCardAudioRecordFile() - onMemoryStorePageRequest() - }, + onMemoryStorePageRequest = onMemoryStorePageRequest, + onSuccessRecord = viewModel::uploadMemoryCardAnswer, uiState = uiState ) } - LifecycleEventEffect(Lifecycle.Event.ON_STOP) { - viewModel.stopMemoryCardAudioRecord() - } - LaunchedEffect(viewModel.sideEffect) { viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle).collect { when (it) { MemoryCardRegistrationSideEffect.LoadError -> - Toast.makeText(context, context.getString(R.string.toast_data_load_error), Toast.LENGTH_SHORT).show() + Toast.makeText( + context, + context.getString(R.string.toast_data_load_error), + Toast.LENGTH_SHORT + ).show() MemoryCardRegistrationSideEffect.RecordingError -> - Toast.makeText(context, context.getString(R.string.toast_audio_recording_error), Toast.LENGTH_SHORT).show() + Toast.makeText( + context, + context.getString(R.string.toast_audio_recording_error), + Toast.LENGTH_SHORT + ).show() MemoryCardRegistrationSideEffect.RecordingPermissionDeniedError -> { - Toast.makeText(context, context.getString(R.string.toast_audio_permission_request), Toast.LENGTH_LONG).show() + Toast.makeText( + context, + context.getString(R.string.toast_audio_permission_request), + Toast.LENGTH_LONG + ).show() activity?.requestRadioAudioPermission() } + + MemoryCardRegistrationSideEffect.NetworkError -> { + Toast.makeText( + context, + context.getString(R.string.toast_network_error), + Toast.LENGTH_LONG + ).show() + onDismissRequest() + } } } } @@ -100,10 +123,34 @@ internal fun MemoryCardRegistrationRoute( @Composable internal fun MemoryCardRegistrationScreen( onDismissRequest: () -> Unit, - onRecordStartRequest: () -> Unit, - onRecordStopRequest: () -> Unit, + onMemoryStorePageRequest: () -> Unit, + onSuccessRecord: (String) -> Unit, uiState: MemoryCardRegistrationUiState, ) { + val context: Context = LocalContext.current + val speechRecognizer: SpeechRecognizer = + remember { SpeechRecognizer.createSpeechRecognizer(context) } + var recordState: RecordState by remember { mutableStateOf(RecordState.INIT) } + + val speechRecognizerIntent: Intent = remember { buildSpeechRecognizerIntent(context) } + val recognitionListener: RecognitionListener = remember { + buildRecognitionListener { result -> + val speechText = result.toString().drop(1).dropLast(1) + onSuccessRecord(speechText) + speechRecognizer.stopListening() + speechRecognizer.cancel() + speechRecognizer.destroy() + recordState = RecordState.COMPLETE + } + } + + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { + speechRecognizer.stopListening() + speechRecognizer.cancel() + speechRecognizer.destroy() + recordState = RecordState.INIT + } + Scaffold( topBar = { AppBar( @@ -129,14 +176,45 @@ internal fun MemoryCardRegistrationScreen( bottomBar = { DefaultButton( onClick = { - when (uiState.recordState) { - RecordState.INIT -> onRecordStartRequest() - RecordState.RECORDING -> onRecordStopRequest() - RecordState.COMPLETE -> {} + when (recordState) { + RecordState.INIT -> { + runCatching { + speechRecognizer.setRecognitionListener(recognitionListener) + speechRecognizer.startListening(speechRecognizerIntent) + } + .onSuccess { + recordState = RecordState.RECORDING + } + .onFailure { + recordState = RecordState.INIT + Toast.makeText( + context, + context.getString(R.string.toast_audio_recording_error), + Toast.LENGTH_LONG + ).show() + } + } + + RecordState.RECORDING -> { + runCatching { speechRecognizer.stopListening() } + .onSuccess { recordState = RecordState.COMPLETE } + .onFailure { + recordState = RecordState.INIT + Toast.makeText( + context, + context.getString(R.string.toast_audio_recording_error), + Toast.LENGTH_LONG + ).show() + } + } + + RecordState.COMPLETE -> { + onMemoryStorePageRequest() + } } }, color = DefaultButtonColor( - containerColor = when (uiState.recordState) { + containerColor = when (recordState) { RecordState.RECORDING -> BL else -> MainGreen } @@ -146,7 +224,7 @@ internal fun MemoryCardRegistrationScreen( .padding(start = 20.dp, end = 20.dp, bottom = 8.dp) ) { Text( - text = when (uiState.recordState) { + text = when (recordState) { RecordState.INIT -> stringResource(R.string.btn_communication_start) RecordState.RECORDING -> stringResource(R.string.btn_communication_end) RecordState.COMPLETE -> stringResource(R.string.btn_communication_complete) @@ -186,7 +264,7 @@ internal fun MemoryCardRegistrationScreen( } SpeechBubble { TypeWriterText( - text = when (uiState.recordState) { + text = when (recordState) { RecordState.INIT -> { stringResource(R.string.text_speechbuble_init) } @@ -208,7 +286,7 @@ internal fun MemoryCardRegistrationScreen( .padding(top = 24.dp) .align(Alignment.CenterHorizontally) ) - if (uiState.recordState == RecordState.RECORDING) { + if (recordState == RecordState.RECORDING) { Image( painter = painterResource(ic_voice_memorycard), contentDescription = "recording", @@ -221,14 +299,59 @@ internal fun MemoryCardRegistrationScreen( } } +private const val LANGUAGE_VALUE = "ko-KR" + +private fun buildSpeechRecognizerIntent(context: Context): Intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, context.packageName) + putExtra(RecognizerIntent.EXTRA_LANGUAGE, LANGUAGE_VALUE) +} + +private fun buildRecognitionListener( + onResult: (ArrayList) -> Unit, +): RecognitionListener = object : RecognitionListener { + override fun onReadyForSpeech(params: Bundle?) { + Log.d("SpeechRecognizer", "onReadyForSpeech params:$params") + } + + override fun onBeginningOfSpeech() { + Log.d("SpeechRecognizer", "onBeginningOfSpeech") + } + + override fun onRmsChanged(rmsdB: Float) { + Log.d("SpeechRecognizer", "sound rms level: $rmsdB") + } + + override fun onBufferReceived(buffer: ByteArray?) {} + + override fun onEndOfSpeech() { + Log.d("SpeechRecognizer", "onEndOfSpeech") + } + + override fun onError(error: Int) { + Log.e("SpeechRecognizer", "onError error:$error") + } + + override fun onResults(results: Bundle?) { + results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.let { + onResult(it) + } ?: Log.d("SpeechRecognizer", "onResults results is null") + } + + override fun onPartialResults(partialResults: Bundle?) { + } + + override fun onEvent(eventType: Int, params: Bundle?) { + } +} + @Preview @Composable private fun MemoryCardRegistrationScreenPreview() { HarmonyTheme { MemoryCardRegistrationScreen( onDismissRequest = {}, - onRecordStartRequest = {}, - onRecordStopRequest = {}, + onMemoryStorePageRequest = {}, + onSuccessRecord = {}, uiState = MemoryCardRegistrationUiState() ) } diff --git a/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/MemoryCardRegistrationViewModel.kt b/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/MemoryCardRegistrationViewModel.kt index 99664204..48de170a 100644 --- a/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/MemoryCardRegistrationViewModel.kt +++ b/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/MemoryCardRegistrationViewModel.kt @@ -1,6 +1,5 @@ package com.teampatch.feature.memorycard.registration -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -8,15 +7,11 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import com.teampatch.core.common.checkRadioAudioPermission -import com.teampatch.core.domain.usecase.memory.AddMemoryCardRecordUseCase +import com.teampatch.core.domain.usecase.memory.AddMemoryCardAnswerUseCase import com.teampatch.core.domain.usecase.memory.GetMemoryCardUseCase import com.teampatch.feature.memorycard.registration.model.MemoryCardRegistrationSideEffect import com.teampatch.feature.memorycard.registration.model.MemoryCardRegistrationUiState -import com.teampatch.feature.memorycard.registration.model.RecordState -import com.teampatch.feature.memorycard.registration.utils.AudioRecorderHelper import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -25,11 +20,9 @@ import kotlinx.coroutines.launch @HiltViewModel internal class MemoryCardRegistrationViewModel @Inject constructor( - @ApplicationContext private val appContext: Context, private val savedStateHandle: SavedStateHandle, private val getMemoryCardUseCase: GetMemoryCardUseCase, - private val addMemoryCardRecordUseCase: AddMemoryCardRecordUseCase, - private val audioRecorderHelper: AudioRecorderHelper, + private val addMemoryCardAnswerUseCase: AddMemoryCardAnswerUseCase, ) : ViewModel() { private val _sideEffect: Channel = Channel() @@ -65,44 +58,12 @@ internal class MemoryCardRegistrationViewModel @Inject constructor( } } - fun startMemoryCardAudioRecord() { + fun uploadMemoryCardAnswer(answer: String) = viewModelScope.launch { try { - if (!appContext.checkRadioAudioPermission()) { - _sideEffect.trySend(MemoryCardRegistrationSideEffect.RecordingPermissionDeniedError) - return - } - - audioRecorderHelper.prepare() - audioRecorderHelper.start() - uiState = uiState.copy(recordState = RecordState.RECORDING) - } catch (e: Exception) { - e.printStackTrace() - _sideEffect.trySend(MemoryCardRegistrationSideEffect.RecordingError) - } - } - - fun stopMemoryCardAudioRecord() { - try { - if (uiState.recordState != RecordState.RECORDING) return - - audioRecorderHelper.stop() - audioRecorderHelper.release() - uiState = uiState.copy(recordState = RecordState.COMPLETE) - } catch (e: Exception) { - e.printStackTrace() - _sideEffect.trySend(MemoryCardRegistrationSideEffect.RecordingError) - } - } - - fun uploadMemoryCardAudioRecordFile() = viewModelScope.launch { - try { - val contentResolver = appContext.contentResolver - contentResolver.openInputStream(audioRecorderHelper.getResultRecordFile())!!.use { - addMemoryCardRecordUseCase(route!!.memoryCardId, uiState.questions.toString(), it) - } + addMemoryCardAnswerUseCase(route!!.memoryCardId, answer) } catch (e: Exception) { e.printStackTrace() - _sideEffect.trySend(MemoryCardRegistrationSideEffect.RecordingError) + _sideEffect.send(MemoryCardRegistrationSideEffect.NetworkError) } } } \ No newline at end of file diff --git a/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/model/MemoryCardRegistrationSideEffect.kt b/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/model/MemoryCardRegistrationSideEffect.kt index 7483fc73..24f069d3 100644 --- a/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/model/MemoryCardRegistrationSideEffect.kt +++ b/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/model/MemoryCardRegistrationSideEffect.kt @@ -5,4 +5,5 @@ internal sealed interface MemoryCardRegistrationSideEffect { data object LoadError : MemoryCardRegistrationSideEffect data object RecordingError : MemoryCardRegistrationSideEffect data object RecordingPermissionDeniedError : MemoryCardRegistrationSideEffect + data object NetworkError : MemoryCardRegistrationSideEffect } \ No newline at end of file diff --git a/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/model/MemoryCardRegistrationUiState.kt b/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/model/MemoryCardRegistrationUiState.kt index 33e1aa9c..f202d888 100644 --- a/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/model/MemoryCardRegistrationUiState.kt +++ b/feature/memorycard-registration/src/main/java/com/teampatch/feature/memorycard/registration/model/MemoryCardRegistrationUiState.kt @@ -5,6 +5,5 @@ internal data class MemoryCardRegistrationUiState( val imageUrl: String? = null, val questions: List = emptyList(), val questionProgressIndex: Int = 0, - val recordState: RecordState = RecordState.INIT, val isLoading: Boolean = true, ) \ No newline at end of file diff --git a/feature/memorycard-registration/src/main/res/values/strings.xml b/feature/memorycard-registration/src/main/res/values/strings.xml index e66096d5..cff4fc5d 100644 --- a/feature/memorycard-registration/src/main/res/values/strings.xml +++ b/feature/memorycard-registration/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ 데이터 로드를 실패하였습니다. 녹음 권한이 필요합니다. 녹음 과정에서 에러가 발생하였습니다. + 녹음 데이터를 업로드 하는 과정에서 에러가 발생하였습니다. \ No newline at end of file