WIP grabador de audio

This commit is contained in:
erik-everardo 2024-01-28 12:27:10 -06:00
parent b539f03ef6
commit 96fd70556b
18 changed files with 472 additions and 38 deletions

View File

@ -308,7 +308,7 @@
<PersistentState>
<option name="values">
<map>
<entry key="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/icons/material/materialicons/password/baseline_password_24.xml" />
<entry key="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/icons/material/materialicons/play_arrow/baseline_play_arrow_24.xml" />
</map>
</option>
</PersistentState>
@ -318,7 +318,7 @@
</option>
<option name="values">
<map>
<entry key="outputName" value="baseline_password_24" />
<entry key="outputName" value="baseline_play_arrow_24" />
<entry key="sourceFile" value="C:\Users\erike\Downloads\face-kiss-wink-heart-solid.svg" />
</map>
</option>

18
.idea/navEditor.xml generated
View File

@ -8,25 +8,13 @@
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="audioDraftsFragment">
<entry key="audiosFragment2">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="130" />
<option name="y" value="18" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="audioRecorderMainFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-74" />
<option name="y" value="17" />
<option name="x" value="40" />
<option name="y" value="40" />
</Point>
</option>
</LayoutPositions>

View File

@ -40,6 +40,7 @@
<activity android:name=".images.image_maker.ui.ImageMakerActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".images.image_chooser.ui.ImageChooserActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".profile.ui.EditProfileActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".audio.recorder.ui.AudioRecorderActivity" android:theme="@style/Theme.Isolaatti" />
<provider
android:authorities="com.isolaatti.provider"
android:name="androidx.core.content.FileProvider"

View File

@ -0,0 +1,11 @@
package com.isolaatti.audio.recorder.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "audio_drafts")
data class AudioDraftEntity(
@PrimaryKey val id: Long,
val name: String,
val audioLocalPath: String
)

View File

@ -0,0 +1,21 @@
package com.isolaatti.audio.recorder.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
@Dao
interface AudiosDraftsDao {
@Insert
fun insertAudioDraft(audioDraftEntity: AudioDraftEntity): Long
@Query("SELECT * FROM audio_drafts WHERE id = :draftId")
fun getAudioDraftById(draftId: Long): AudioDraftEntity
@Query("SELECT * FROM audio_drafts WHERE id < :before ORDER BY id DESC LIMIT :count")
fun getDrafts(count: Int, before: Long): List<AudioDraftEntity>
@Query("DELETE FROM audio_drafts WHERE id in (:draftIds)")
fun deleteDrafts(draftIds: LongArray)
}

View File

@ -1,6 +1,11 @@
package com.isolaatti.audio.recorder.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
class AudioDraftsFragment : Fragment() {
}

View File

@ -1,6 +1,219 @@
package com.isolaatti.audio.recorder.ui
import androidx.activity.ComponentActivity
import android.Manifest
import android.content.Intent
import android.media.MediaRecorder
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.PermissionChecker
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.databinding.ActivityAudioRecorderBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import java.io.IOException
import java.util.UUID
@AndroidEntryPoint
class AudioRecorderActivity : AppCompatActivity() {
companion object {
const val LOG_TAG = "AudioRecorderActivity"
}
private lateinit var binding: ActivityAudioRecorderBinding
private var audioRecorder: MediaRecorder? = null
private val audioUID = UUID.randomUUID()
private lateinit var outputFile: String
private val requestAudioPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if(it) {
startRecording()
} else {
showPermissionRationale()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAudioRecorderBinding.inflate(layoutInflater)
setContentView(binding.root)
File("${filesDir.absolutePath}/audios/").let {
if(!it.isDirectory) {
it.mkdir()
}
}
outputFile = "${filesDir.absolutePath}/audios/${audioUID}.3gp"
setupListeners()
}
private fun checkRecordAudioPermission(): Boolean {
return when {
PermissionChecker.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PERMISSION_GRANTED -> true
ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO) -> {
showPermissionRationale()
false
}
else -> {
requestAudioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
false
}
}
}
private fun showPermissionRationale() {
MaterialAlertDialogBuilder(this)
.setTitle("Record audio permission")
.setMessage("We need permission to access your microphone so that you can record your audio. Go to settings.")
.setPositiveButton("Go to settings"){_, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", packageName, null)
startActivity(intent)
}
.setNegativeButton("No, thanks", null)
.show()
}
private fun setupListeners() {
binding.recordButton.setOnClickListener {
if(checkRecordAudioPermission()) {
Log.d(LOG_TAG, "Starts recording")
startRecording()
} else {
Log.d(LOG_TAG, "Failed to start recording: mic permission not granted")
}
}
binding.stopRecording.setOnClickListener {
stopRecording()
}
binding.cancelButton.setOnClickListener {
discardRecording()
}
binding.pauseRecording.setOnClickListener {
pauseRecording()
}
binding.acceptButton.setOnClickListener {
acceptRecording()
}
binding.playPauseButton.setOnClickListener {
}
}
// region timer
private var timer: Job? = null
private var timerValue = 0
private fun setDisplayTime(seconds: Int, showTotalTime: Boolean) {
val stringBuilder = StringBuilder()
stringBuilder.append(seconds)
binding.time.text = stringBuilder.toString()
}
private fun startTimerRecorder() {
timer = CoroutineScope(Dispatchers.Main).launch {
setDisplayTime(timerValue, false)
delay(1000)
timerValue++
startTimerRecorder()
}
}
private fun startTimerPlayer() {
timer?.cancel()
timer = CoroutineScope(Dispatchers.Main).launch {
setDisplayTime(timerValue, true)
delay(1000)
timerValue++
}
}
private fun stopTimer() {
timer?.cancel()
}
// end region
// region record functions
private fun startRecording() {
audioRecorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
setOutputFile(outputFile)
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
try {
prepare()
start()
timerValue = 0
startTimerRecorder()
binding.viewAnimator.displayedChild = 1
} catch(e: IOException) {
Log.e(LOG_TAG, "prepare() failed\n${e.message}")
}
}
}
private fun stopRecording() {
audioRecorder?.apply {
stop()
release()
}
stopTimer()
audioRecorder = null
// shows third state: audio recorded
binding.viewAnimator.displayedChild = 2
}
private fun pauseRecording() {
audioRecorder?.pause()
stopTimer()
}
private fun discardRecording(){
File(outputFile).apply {
try {
delete()
} catch(e: SecurityException) {
Log.e(LOG_TAG, "Could not delete file\n${e.message}")
}
}
binding.viewAnimator.displayedChild = 0
}
// end region
private fun acceptRecording() {
}
private fun playPauseRecording() {
}
class AudioRecorderActivity : ComponentActivity() {
}

View File

@ -1,8 +0,0 @@
package com.isolaatti.audio.recorder.ui
import android.media.MediaRecorder
import androidx.fragment.app.Fragment
class AudioRecorderMainFragment : Fragment() {
}

View File

@ -2,14 +2,17 @@ package com.isolaatti.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.isolaatti.audio.recorder.data.AudioDraftEntity
import com.isolaatti.audio.recorder.data.AudiosDraftsDao
import com.isolaatti.auth.data.local.UserInfoDao
import com.isolaatti.auth.data.local.UserInfoEntity
import com.isolaatti.settings.data.KeyValueDao
import com.isolaatti.settings.data.KeyValueEntity
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class], version = 2)
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class], version = 3)
abstract class AppDatabase : RoomDatabase() {
abstract fun keyValueDao(): KeyValueDao
abstract fun userInfoDao(): UserInfoDao
abstract fun audioDrafts(): AudiosDraftsDao
}

View File

@ -1,5 +1,6 @@
package com.isolaatti.posting.posts.ui
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -9,6 +10,7 @@ import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import com.isolaatti.audio.recorder.ui.AudioRecorderActivity
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
import com.isolaatti.posting.link_creator.presentation.LinkCreatorViewModel
@ -63,6 +65,10 @@ class MarkdownEditingFragment : Fragment(){
binding.addLinkButton.setOnClickListener {
insertLink()
}
binding.addAudioButton.setOnClickListener {
requireContext().startActivity(Intent(requireContext(), AudioRecorderActivity::class.java))
}
}
private fun setupObservers(){

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,6h12v12H6z"/>
</vector>

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:navigationIcon="@drawable/baseline_close_24"/>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/time"
app:layout_constraintTop_toBottomOf="@id/toolbar"
android:layout_margin="24dp"
style="?attr/materialCardViewFilledStyle">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="@navigation/audio_recorder_navigation"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"/>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/card_controls"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="16dp"
android:textSize="24sp"
tools:text="00:00"
android:textAlignment="center"/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
style="?attr/materialCardViewFilledStyle"
android:layout_margin="24dp">
<ViewAnimator
android:id="@+id/viewAnimator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inAnimation="@anim/nav_default_enter_anim"
android:outAnimation="@anim/nav_default_exit_anim">
<!-- initial state: show only start record button-->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/record_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_circle_24"
app:iconTint="@color/danger"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- second state: recording-->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/stop_recording"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_stop_24"
app:layout_constraintEnd_toStartOf="@+id/pause_recording"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pause_recording"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_pause_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/stop_recording"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- third state: recorded-->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_close_24"
app:layout_constraintEnd_toStartOf="@+id/play_pause_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/play_pause_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_play_arrow_24"
app:layout_constraintEnd_toStartOf="@+id/accept_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/cancel_button"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/accept_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_check_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/play_pause_button"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ViewAnimator>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -40,5 +40,28 @@
style="@style/Widget.Material3.Button.IconButton"
app:icon="@drawable/baseline_add_link_24" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="30dp"
android:layout_marginHorizontal="24dp"
style="?attr/materialCardViewFilledStyle">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Audio attachment"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/add_audio_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Attach audio" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -1,14 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/audio_recorder_navigation"
app:startDestination="@id/audioRecorderMainFragment">
<fragment
android:id="@+id/audioRecorderMainFragment"
android:name="com.isolaatti.audio.recorder.ui.AudioRecorderMainFragment"
android:label="AudioRecorderMainFragment" />
<fragment
android:id="@+id/audioDraftsFragment"
android:name="com.isolaatti.audio.recorder.ui.AudioDraftsFragment"
android:label="AudioDraftsFragment" />
android:id="@+id/audio_recorder_navigation">
</navigation>

View File

@ -4,4 +4,5 @@
<color name="purple_lighter">#3F0095</color>
<color name="surface">#1D1725</color>
<color name="on_surface">#FFFFFF</color>
<color name="danger">#750606</color>
</resources>