diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 590b0f54..42c3b6f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -104,4 +104,5 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.navigation.compose) + implementation(libs.accompanist.permissions) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab274e8f..03b0fcbe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ android:name="android.permission.FOREGROUND_SERVICE" tools:node="remove"/> - + > + + @Delete + suspend fun deleteVoiceNote(voiceNote: VoiceNote) +} diff --git a/app/src/main/java/com/kin/easynotes/data/local/database/NoteDatabase.kt b/app/src/main/java/com/kin/easynotes/data/local/database/NoteDatabase.kt index fa94a670..545735c2 100644 --- a/app/src/main/java/com/kin/easynotes/data/local/database/NoteDatabase.kt +++ b/app/src/main/java/com/kin/easynotes/data/local/database/NoteDatabase.kt @@ -4,14 +4,17 @@ import androidx.room.Database import androidx.room.RoomDatabase import com.kin.easynotes.core.constant.DatabaseConst import com.kin.easynotes.data.local.dao.NoteDao +import com.kin.easynotes.data.local.dao.VoiceNoteDao +import com.kin.easynotes.data.local.models.VoiceNote import com.kin.easynotes.domain.model.Note @Database( - entities = [Note::class], + entities = [Note::class, VoiceNote::class], version = DatabaseConst.NOTES_DATABASE_VERSION, exportSchema = false ) abstract class NoteDatabase : RoomDatabase() { abstract fun noteDao(): NoteDao + abstract fun voiceNoteDao(): VoiceNoteDao } \ No newline at end of file diff --git a/app/src/main/java/com/kin/easynotes/data/local/database/NoteDatabaseProvider.kt b/app/src/main/java/com/kin/easynotes/data/local/database/NoteDatabaseProvider.kt index f087a6ce..3aaa5400 100644 --- a/app/src/main/java/com/kin/easynotes/data/local/database/NoteDatabaseProvider.kt +++ b/app/src/main/java/com/kin/easynotes/data/local/database/NoteDatabaseProvider.kt @@ -1,12 +1,12 @@ package com.kin.easynotes.data.local.database - import android.app.Application import androidx.room.Room import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.kin.easynotes.core.constant.DatabaseConst import com.kin.easynotes.data.local.dao.NoteDao +import com.kin.easynotes.data.local.dao.VoiceNoteDao class NoteDatabaseProvider(private val application: Application) { @@ -21,10 +21,12 @@ class NoteDatabaseProvider(private val application: Application) { } private fun buildDatabase(): NoteDatabase { - return Room.databaseBuilder(application.applicationContext, + return Room.databaseBuilder( + application.applicationContext, NoteDatabase::class.java, - DatabaseConst.NOTES_DATABASE_FILE_NAME) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_2_4) + DatabaseConst.NOTES_DATABASE_FILE_NAME + ) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) .build() } @@ -37,9 +39,11 @@ class NoteDatabaseProvider(private val application: Application) { fun noteDao(): NoteDao { return instance().noteDao() } -} - + fun voiceNoteDao(): VoiceNoteDao { + return instance().voiceNoteDao() + } +} private val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { @@ -59,9 +63,8 @@ private val MIGRATION_3_4 = object : Migration(3, 4) { } } -private val MIGRATION_2_4 = object : Migration(2, 4) { +private val MIGRATION_4_5 = object : Migration(4, 5) { override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `pinned` INTEGER NOT NULL DEFAULT 0") - db.execSQL("ALTER TABLE `notes-table` ADD COLUMN `encrypted` INTEGER NOT NULL DEFAULT 0") + db.execSQL("CREATE TABLE IF NOT EXISTS `voice_notes` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `filePath` TEXT NOT NULL, `duration` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)") } } diff --git a/app/src/main/java/com/kin/easynotes/data/local/models/VoiceNote.kt b/app/src/main/java/com/kin/easynotes/data/local/models/VoiceNote.kt new file mode 100644 index 00000000..2c73c685 --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/data/local/models/VoiceNote.kt @@ -0,0 +1,13 @@ +package com.kin.easynotes.data.local.models + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "voice_notes") +data class VoiceNote( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val filePath: String, + val duration: Long, + val createdAt: Long +) diff --git a/app/src/main/java/com/kin/easynotes/data/repository/VoiceNotesRepository.kt b/app/src/main/java/com/kin/easynotes/data/repository/VoiceNotesRepository.kt new file mode 100644 index 00000000..c71de539 --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/data/repository/VoiceNotesRepository.kt @@ -0,0 +1,21 @@ +package com.kin.easynotes.data.repository + +import com.kin.easynotes.data.local.dao.VoiceNoteDao +import com.kin.easynotes.data.local.models.VoiceNote +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class VoiceNotesRepository @Inject constructor( + private val voiceNoteDao: VoiceNoteDao +) { + + fun getAllVoiceNotes(): Flow> = voiceNoteDao.getAllVoiceNotes() + + suspend fun insertVoiceNote(voiceNote: VoiceNote) { + voiceNoteDao.insertVoiceNote(voiceNote) + } + + suspend fun deleteVoiceNote(voiceNote: VoiceNote) { + voiceNoteDao.deleteVoiceNote(voiceNote) + } +} diff --git a/app/src/main/java/com/kin/easynotes/di/DataModule.kt b/app/src/main/java/com/kin/easynotes/di/DataModule.kt index 942a5288..b8ec3ff9 100644 --- a/app/src/main/java/com/kin/easynotes/di/DataModule.kt +++ b/app/src/main/java/com/kin/easynotes/di/DataModule.kt @@ -4,10 +4,12 @@ import android.app.Application import android.os.Handler import android.content.Context import android.os.Looper +import com.kin.easynotes.data.local.dao.VoiceNoteDao import com.kin.easynotes.data.local.database.NoteDatabaseProvider import com.kin.easynotes.data.repository.ImportExportRepository import com.kin.easynotes.data.repository.NoteRepositoryImpl import com.kin.easynotes.data.repository.SettingsRepositoryImpl +import com.kin.easynotes.data.repository.VoiceNotesRepository import com.kin.easynotes.presentation.components.EncryptionHelper import dagger.Module import dagger.Provides @@ -99,4 +101,16 @@ object ApplicationModule { fun provideHandler(): Handler { return Handler(Looper.getMainLooper()) } + + @Provides + @Singleton + fun provideVoiceNoteDao(noteDatabaseProvider: NoteDatabaseProvider): VoiceNoteDao { + return noteDatabaseProvider.voiceNoteDao() + } + + @Provides + @Singleton + fun provideVoiceNotesRepository(voiceNoteDao: VoiceNoteDao): VoiceNotesRepository { + return VoiceNotesRepository(voiceNoteDao) + } } diff --git a/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/AddVoiceNoteUseCase.kt b/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/AddVoiceNoteUseCase.kt new file mode 100644 index 00000000..c5873800 --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/AddVoiceNoteUseCase.kt @@ -0,0 +1,13 @@ +package com.kin.easynotes.domain.usecase.voice_notes + +import com.kin.easynotes.data.local.models.VoiceNote +import com.kin.easynotes.data.repository.VoiceNotesRepository +import javax.inject.Inject + +class AddVoiceNoteUseCase @Inject constructor( + private val repository: VoiceNotesRepository +) { + suspend operator fun invoke(voiceNote: VoiceNote) { + repository.insertVoiceNote(voiceNote) + } +} diff --git a/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/DeleteVoiceNoteUseCase.kt b/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/DeleteVoiceNoteUseCase.kt new file mode 100644 index 00000000..3d0d0ab7 --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/DeleteVoiceNoteUseCase.kt @@ -0,0 +1,13 @@ +package com.kin.easynotes.domain.usecase.voice_notes + +import com.kin.easynotes.data.local.models.VoiceNote +import com.kin.easynotes.data.repository.VoiceNotesRepository +import javax.inject.Inject + +class DeleteVoiceNoteUseCase @Inject constructor( + private val repository: VoiceNotesRepository +) { + suspend operator fun invoke(voiceNote: VoiceNote) { + repository.deleteVoiceNote(voiceNote) + } +} diff --git a/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/GetAllVoiceNotesUseCase.kt b/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/GetAllVoiceNotesUseCase.kt new file mode 100644 index 00000000..5e47a63a --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/domain/usecase/voice_notes/GetAllVoiceNotesUseCase.kt @@ -0,0 +1,14 @@ +package com.kin.easynotes.domain.usecase.voice_notes + +import com.kin.easynotes.data.local.models.VoiceNote +import com.kin.easynotes.data.repository.VoiceNotesRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetAllVoiceNotesUseCase @Inject constructor( + private val repository: VoiceNotesRepository +) { + operator fun invoke(): Flow> { + return repository.getAllVoiceNotes() + } +} diff --git a/app/src/main/java/com/kin/easynotes/presentation/components/VoiceNotesButton.kt b/app/src/main/java/com/kin/easynotes/presentation/components/VoiceNotesButton.kt new file mode 100644 index 00000000..7ea682a9 --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/presentation/components/VoiceNotesButton.kt @@ -0,0 +1,14 @@ +package com.kin.easynotes.presentation.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardVoice +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable + +@Composable +fun VoiceNotesButton(onClick: () -> Unit) { + IconButton(onClick = onClick) { + Icon(imageVector = Icons.Default.KeyboardVoice, contentDescription = "Voice Notes") + } +} diff --git a/app/src/main/java/com/kin/easynotes/presentation/navigation/NavHost.kt b/app/src/main/java/com/kin/easynotes/presentation/navigation/NavHost.kt index 10f6fd26..f6421826 100644 --- a/app/src/main/java/com/kin/easynotes/presentation/navigation/NavHost.kt +++ b/app/src/main/java/com/kin/easynotes/presentation/navigation/NavHost.kt @@ -17,6 +17,7 @@ import com.kin.easynotes.presentation.screens.edit.EditNoteView import com.kin.easynotes.presentation.screens.home.HomeView import com.kin.easynotes.presentation.screens.settings.model.SettingsViewModel import com.kin.easynotes.presentation.screens.terms.TermsScreen +import com.kin.easynotes.presentation.screens.voice_notes.VoiceNotesScreen import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -44,6 +45,7 @@ fun AppNavHost(settingsModel: SettingsViewModel,navController: NavHostController ) ) }, + onVoiceNotesClicked = { navController.navigate(NavRoutes.VoiceNotes.route) }, settingsModel = settingsModel ) } @@ -87,6 +89,10 @@ fun AppNavHost(settingsModel: SettingsViewModel,navController: NavHostController } } + animatedComposable(NavRoutes.VoiceNotes.route) { + VoiceNotesScreen() + } + settingScreens.forEach { (route, screen) -> if (route == NavRoutes.Settings.route) { slideInComposable(route) { diff --git a/app/src/main/java/com/kin/easynotes/presentation/navigation/NavRoutes.kt b/app/src/main/java/com/kin/easynotes/presentation/navigation/NavRoutes.kt index 702c4768..2b0b1ed8 100644 --- a/app/src/main/java/com/kin/easynotes/presentation/navigation/NavRoutes.kt +++ b/app/src/main/java/com/kin/easynotes/presentation/navigation/NavRoutes.kt @@ -26,6 +26,7 @@ sealed class NavRoutes(val route: String) { } data object Terms : NavRoutes("terms") data object Settings : NavRoutes("settings") + data object VoiceNotes : NavRoutes("voice_notes") data object ColorStyles : NavRoutes("settings/color_styles") data object Language : NavRoutes("settings/language") data object Cloud : NavRoutes("settings/cloud") diff --git a/app/src/main/java/com/kin/easynotes/presentation/screens/home/HomeScreen.kt b/app/src/main/java/com/kin/easynotes/presentation/screens/home/HomeScreen.kt index 13ba3897..afc09866 100644 --- a/app/src/main/java/com/kin/easynotes/presentation/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/kin/easynotes/presentation/screens/home/HomeScreen.kt @@ -37,6 +37,7 @@ import com.kin.easynotes.presentation.components.SelectAllButton import com.kin.easynotes.presentation.components.SettingsButton import com.kin.easynotes.presentation.components.TitleText import com.kin.easynotes.presentation.components.VaultButton +import com.kin.easynotes.presentation.components.VoiceNotesButton import com.kin.easynotes.presentation.components.defaultScreenEnterAnimation import com.kin.easynotes.presentation.components.defaultScreenExitAnimation import com.kin.easynotes.presentation.screens.home.viewmodel.HomeViewModel @@ -54,7 +55,8 @@ fun HomeView ( viewModel: HomeViewModel = hiltViewModel(), settingsModel: SettingsViewModel, onSettingsClicked: () -> Unit, - onNoteClicked: (Int, Boolean) -> Unit + onNoteClicked: (Int, Boolean) -> Unit, + onVoiceNotesClicked: () -> Unit ) { val context = LocalContext.current if (viewModel.isPasswordPromptVisible.value) { @@ -113,7 +115,8 @@ fun HomeView ( viewModel.toggleIsVaultMode(false) viewModel.encryptionHelper.removePassword() } - } + }, + onVoiceNotesClicked = onVoiceNotesClicked ) } }, @@ -217,7 +220,8 @@ private fun NotesSearchBar( onQueryChange: (String) -> Unit, onSettingsClick: () -> Unit, onVaultClicked: () -> Unit, - onClearClick: () -> Unit + onClearClick: () -> Unit, + onVoiceNotesClicked: () -> Unit ) { SearchBar( modifier = Modifier @@ -231,6 +235,7 @@ private fun NotesSearchBar( if (query.isNotBlank()) { CloseButton(contentDescription = "Clear", onCloseClicked = onClearClick) } + VoiceNotesButton(onClick = onVoiceNotesClicked) if (settingsModel.settings.value.vaultSettingEnabled) { VaultButton(viewModel.isVaultMode.value) { onVaultClicked() } } @@ -258,4 +263,4 @@ fun sorter(descending: Boolean): Comparator { } else { compareBy { it.createdAt } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNoteItem.kt b/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNoteItem.kt new file mode 100644 index 00000000..7573d4e6 --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNoteItem.kt @@ -0,0 +1,55 @@ +package com.kin.easynotes.presentation.screens.voice_notes + +import android.media.MediaPlayer +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.kin.easynotes.data.local.models.VoiceNote +import com.kin.easynotes.presentation.utils.TimerUtil + +@Composable +fun VoiceNoteItem(voiceNote: VoiceNote, onDelete: (VoiceNote) -> Unit) { + val mediaPlayer = remember { MediaPlayer() } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .clickable { /* Handle item click if needed */ }, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = TimerUtil.formatDuration(voiceNote.duration), modifier = Modifier.weight(1f)) + IconButton(onClick = { + if (mediaPlayer.isPlaying) { + mediaPlayer.pause() + } else { + mediaPlayer.apply { + reset() + setDataSource(voiceNote.filePath) + prepare() + start() + } + } + }) { + Icon(imageVector = Icons.Default.PlayArrow, contentDescription = "Play/Pause") + } + Spacer(modifier = Modifier.width(8.dp)) + IconButton(onClick = { onDelete(voiceNote) }) { + Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete") + } + } +} diff --git a/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNotesScreen.kt b/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNotesScreen.kt new file mode 100644 index 00000000..29bad5a1 --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNotesScreen.kt @@ -0,0 +1,73 @@ +package com.kin.easynotes.presentation.screens.voice_notes + +import android.Manifest +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) +@Composable +fun VoiceNotesScreen(viewModel: VoiceNotesViewModel = hiltViewModel()) { + val voiceNotes by viewModel.voiceNotes.collectAsState() + var isRecording by remember { mutableStateOf(false) } + val permissionState = rememberPermissionState(permission = Manifest.permission.RECORD_AUDIO) + + Scaffold( + topBar = { + TopAppBar(title = { Text("Voice Notes") }) + }, + floatingActionButton = { + FloatingActionButton(onClick = { + if (permissionState.status.isGranted) { + isRecording = if (!isRecording) { + viewModel.startRecording() + true + } else { + viewModel.stopRecording() + false + } + } else { + permissionState.launchPermissionRequest() + } + }) { + Icon(imageVector = Icons.Default.Mic, contentDescription = "Record") + } + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + LazyColumn { + items(voiceNotes) { voiceNote -> + VoiceNoteItem(voiceNote = voiceNote, onDelete = { + viewModel.deleteVoiceNote(it) + }) + } + } + } + } +} diff --git a/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNotesViewModel.kt b/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNotesViewModel.kt new file mode 100644 index 00000000..ca01e14a --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/presentation/screens/voice_notes/VoiceNotesViewModel.kt @@ -0,0 +1,123 @@ +package com.kin.easynotes.presentation.screens.voice_notes + +import android.content.Context +import android.media.MediaRecorder +import android.os.Build +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kin.easynotes.data.local.models.VoiceNote +import com.kin.easynotes.domain.usecase.voice_notes.AddVoiceNoteUseCase +import com.kin.easynotes.domain.usecase.voice_notes.DeleteVoiceNoteUseCase +import com.kin.easynotes.domain.usecase.voice_notes.GetAllVoiceNotesUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.io.File +import java.io.IOException +import javax.inject.Inject + +@HiltViewModel +class VoiceNotesViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val addVoiceNoteUseCase: AddVoiceNoteUseCase, + private val deleteVoiceNoteUseCase: DeleteVoiceNoteUseCase, + private val getAllVoiceNotesUseCase: GetAllVoiceNotesUseCase +) : ViewModel() { + + private val _voiceNotes = MutableStateFlow>(emptyList()) + val voiceNotes = _voiceNotes.asStateFlow() + + private var mediaRecorder: MediaRecorder? = null + private var audioFile: File? = null + + val voiceNotesDir = File(context.filesDir, "voice_notes") + + init { + if (!voiceNotesDir.exists()) { + voiceNotesDir.mkdirs() + } + loadVoiceNotes() + } + + private fun loadVoiceNotes() { + viewModelScope.launch { + getAllVoiceNotesUseCase().collectLatest { + _voiceNotes.value = it + } + } + } + + fun startRecording() { + val fileName = "voice_note_${System.currentTimeMillis()}.m4a" + audioFile = File(voiceNotesDir, fileName) + + mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MediaRecorder(context) + } else { + MediaRecorder() + } + + mediaRecorder?.apply { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + setOutputFile(audioFile?.absolutePath) + try { + prepare() + start() + } catch (e: IOException) { + // Handle exception + } + } + } + + fun pauseRecording() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder?.pause() + } + } + + fun resumeRecording() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder?.resume() + } + } + + fun stopRecording() { + mediaRecorder?.apply { + stop() + release() + } + mediaRecorder = null + + audioFile?.let { file -> + val duration = getAudioDuration(file.absolutePath) + val voiceNote = VoiceNote( + filePath = file.absolutePath, + duration = duration, + createdAt = System.currentTimeMillis() + ) + viewModelScope.launch { + addVoiceNoteUseCase(voiceNote) + } + } + audioFile = null + } + + fun deleteVoiceNote(voiceNote: VoiceNote) { + viewModelScope.launch { + deleteVoiceNoteUseCase(voiceNote) + File(voiceNote.filePath).delete() + } + } + + private fun getAudioDuration(filePath: String): Long { + val mediaMetadataRetriever = android.media.MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(filePath) + val durationStr = mediaMetadataRetriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_DURATION) + return durationStr?.toLongOrNull() ?: 0L + } +} diff --git a/app/src/main/java/com/kin/easynotes/presentation/utils/TimerUtil.kt b/app/src/main/java/com/kin/easynotes/presentation/utils/TimerUtil.kt new file mode 100644 index 00000000..9dac6c5a --- /dev/null +++ b/app/src/main/java/com/kin/easynotes/presentation/utils/TimerUtil.kt @@ -0,0 +1,11 @@ +package com.kin.easynotes.presentation.utils + +import java.util.concurrent.TimeUnit + +object TimerUtil { + fun formatDuration(duration: Long): String { + val minutes = TimeUnit.MILLISECONDS.toMinutes(duration) + val seconds = TimeUnit.MILLISECONDS.toSeconds(duration) % 60 + return String.format("%02d:%02d", minutes, seconds) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a49316eb..a5dad639 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ material3 = "1.2.1" navigationCompose = "2.7.7" appcompat = "1.7.0" glanceAppwidget = "1.1.0" +accompanistPermissions = "0.32.0" [libraries] # Room @@ -40,6 +41,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } #Hilt hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidCompiler" }