Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -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
Expand All @@ -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<PagingData<MemoryCard>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ interface MemoryCardRepository {
audioFile: InputStream,
)

suspend fun addAnswer(
memoryCardId: String,
answer: String,
)

/**
* 해당 추억카드 질문들을 가져오는 함수입니다.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
}
}
}
}
Expand All @@ -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(
Expand All @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -186,7 +264,7 @@ internal fun MemoryCardRegistrationScreen(
}
SpeechBubble {
TypeWriterText(
text = when (uiState.recordState) {
text = when (recordState) {
RecordState.INIT -> {
stringResource(R.string.text_speechbuble_init)
}
Expand All @@ -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",
Expand All @@ -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<String>) -> 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()
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
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
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
Expand All @@ -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<MemoryCardRegistrationSideEffect> = Channel()
Expand Down Expand Up @@ -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)
}
}
}
Loading