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" }