WIP grabador de audio
This commit is contained in:
parent
3ff49d73ce
commit
5084fe337d
2
.idea/navEditor.xml
generated
2
.idea/navEditor.xml
generated
@ -8,7 +8,7 @@
|
|||||||
<LayoutPositions>
|
<LayoutPositions>
|
||||||
<option name="myPositions">
|
<option name="myPositions">
|
||||||
<map>
|
<map>
|
||||||
<entry key="audiosFragment2">
|
<entry key="audioDraftsFragment">
|
||||||
<value>
|
<value>
|
||||||
<LayoutPositions>
|
<LayoutPositions>
|
||||||
<option name="myPosition">
|
<option name="myPosition">
|
||||||
|
|||||||
@ -4,7 +4,11 @@ import androidx.media3.common.Player
|
|||||||
import com.isolaatti.audio.common.data.AudiosApi
|
import com.isolaatti.audio.common.data.AudiosApi
|
||||||
import com.isolaatti.audio.common.data.AudiosRepositoryImpl
|
import com.isolaatti.audio.common.data.AudiosRepositoryImpl
|
||||||
import com.isolaatti.audio.common.domain.AudiosRepository
|
import com.isolaatti.audio.common.domain.AudiosRepository
|
||||||
|
import com.isolaatti.audio.drafts.data.AudioDraftsRepositoryImpl
|
||||||
|
import com.isolaatti.audio.drafts.data.AudiosDraftsDao
|
||||||
|
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||||
import com.isolaatti.connectivity.RetrofitClient
|
import com.isolaatti.connectivity.RetrofitClient
|
||||||
|
import com.isolaatti.database.AppDatabase
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@ -22,4 +26,14 @@ class Module {
|
|||||||
fun provideAudiosRepository(audiosApi: AudiosApi): AudiosRepository {
|
fun provideAudiosRepository(audiosApi: AudiosApi): AudiosRepository {
|
||||||
return AudiosRepositoryImpl(audiosApi)
|
return AudiosRepositoryImpl(audiosApi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideAudioDraftsDao(database: AppDatabase): AudiosDraftsDao {
|
||||||
|
return database.audioDrafts()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideAudioDraftsRepository(audiosDraftsDao: AudiosDraftsDao): AudioDraftsRepository {
|
||||||
|
return AudioDraftsRepositoryImpl(audiosDraftsDao)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -10,4 +10,21 @@ abstract class Playable {
|
|||||||
* Image url, null indicating no image should be shown
|
* Image url, null indicating no image should be shown
|
||||||
*/
|
*/
|
||||||
abstract val thumbnail: String?
|
abstract val thumbnail: String?
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Playable
|
||||||
|
|
||||||
|
if (uri != other.uri) return false
|
||||||
|
return thumbnail == other.thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = uri.hashCode()
|
||||||
|
result = 31 * result + (thumbnail?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@ import androidx.room.PrimaryKey
|
|||||||
|
|
||||||
@Entity(tableName = "audio_drafts")
|
@Entity(tableName = "audio_drafts")
|
||||||
data class AudioDraftEntity(
|
data class AudioDraftEntity(
|
||||||
@PrimaryKey val id: Long,
|
@PrimaryKey(autoGenerate = true) val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val audioLocalPath: String
|
val audioLocalPath: String
|
||||||
)
|
)
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.isolaatti.audio.drafts.data
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.isolaatti.MyApplication
|
||||||
|
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||||
|
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AudioDraftsRepositoryImpl(private val audiosDraftsDao: AudiosDraftsDao) : AudioDraftsRepository {
|
||||||
|
override fun saveAudioDraft(name: String, relativePath: String): Flow<AudioDraft> = flow {
|
||||||
|
val entity = AudioDraftEntity(0, name, relativePath)
|
||||||
|
val insertedEntityId = audiosDraftsDao.insertAudioDraft(AudioDraftEntity(0, name, relativePath))
|
||||||
|
|
||||||
|
emit(AudioDraft.fromEntity(entity.copy(id = insertedEntityId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAudioDrafts(): Flow<List<AudioDraft>> = flow {
|
||||||
|
emit(audiosDraftsDao.getDrafts().map { AudioDraft.fromEntity(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteDrafts(draftIds: LongArray): Flow<Boolean> = flow {
|
||||||
|
val drafts = audiosDraftsDao.getAudioDraftsByIds(draftIds)
|
||||||
|
audiosDraftsDao.deleteDrafts(drafts)
|
||||||
|
|
||||||
|
try {
|
||||||
|
for(draft in drafts) {
|
||||||
|
File(MyApplication.myApp.applicationContext.filesDir, draft.audioLocalPath).delete()
|
||||||
|
}
|
||||||
|
} catch(securityException: SecurityException) {
|
||||||
|
Log.e("AudioDraftsRepositoryImpl", "Could not delete file\n${securityException.message}")
|
||||||
|
}
|
||||||
|
emit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renameDraft(draftId: Long, name: String): Flow<Boolean> = flow {
|
||||||
|
val rowsAffected = audiosDraftsDao.renameDraft(draftId, name)
|
||||||
|
|
||||||
|
emit(rowsAffected > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,18 +4,25 @@ import androidx.room.Dao
|
|||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface AudiosDraftsDao {
|
interface AudiosDraftsDao {
|
||||||
@Insert
|
@Insert
|
||||||
fun insertAudioDraft(audioDraftEntity: AudioDraftEntity): Long
|
suspend fun insertAudioDraft(audioDraftEntity: AudioDraftEntity): Long
|
||||||
|
|
||||||
@Query("SELECT * FROM audio_drafts WHERE id = :draftId")
|
@Query("SELECT * FROM audio_drafts WHERE id = :draftId")
|
||||||
fun getAudioDraftById(draftId: Long): AudioDraftEntity
|
suspend fun getAudioDraftById(draftId: Long): AudioDraftEntity
|
||||||
|
|
||||||
@Query("SELECT * FROM audio_drafts WHERE id < :before ORDER BY id DESC LIMIT :count")
|
@Query("SELECT * FROM audio_drafts WHERE id in (:draftId)")
|
||||||
fun getDrafts(count: Int, before: Long): List<AudioDraftEntity>
|
suspend fun getAudioDraftsByIds(draftId: LongArray): Array<AudioDraftEntity>
|
||||||
|
|
||||||
@Query("DELETE FROM audio_drafts WHERE id in (:draftIds)")
|
@Query("SELECT * FROM audio_drafts ORDER BY id DESC")
|
||||||
fun deleteDrafts(draftIds: LongArray)
|
suspend fun getDrafts(): List<AudioDraftEntity>
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteDrafts(draft: Array<AudioDraftEntity>)
|
||||||
|
|
||||||
|
@Query("UPDATE audio_drafts SET name = :name WHERE id = :id")
|
||||||
|
suspend fun renameDraft(id: Long, name: String): Int
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.isolaatti.MyApplication
|
import com.isolaatti.MyApplication
|
||||||
import com.isolaatti.audio.common.domain.Playable
|
import com.isolaatti.audio.common.domain.Playable
|
||||||
|
import com.isolaatti.audio.drafts.data.AudioDraftEntity
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
data class AudioDraft(val id: Long, val name: String, val localStorageRelativePath: String) : Playable() {
|
data class AudioDraft(val id: Long, val name: String, val localStorageRelativePath: String) : Playable() {
|
||||||
@ -15,4 +16,10 @@ data class AudioDraft(val id: Long, val name: String, val localStorageRelativePa
|
|||||||
val appFiles = MyApplication.myApp.applicationContext.filesDir
|
val appFiles = MyApplication.myApp.applicationContext.filesDir
|
||||||
return File(appFiles, localStorageRelativePath).toUri()
|
return File(appFiles, localStorageRelativePath).toUri()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromEntity(audioDraftEntity: AudioDraftEntity): AudioDraft {
|
||||||
|
return AudioDraft(audioDraftEntity.id, audioDraftEntity.name, audioDraftEntity.audioLocalPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.isolaatti.audio.drafts.domain.repository
|
||||||
|
|
||||||
|
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface AudioDraftsRepository {
|
||||||
|
fun saveAudioDraft(name: String, relativePath: String): Flow<AudioDraft>
|
||||||
|
|
||||||
|
fun getAudioDrafts(): Flow<List<AudioDraft>>
|
||||||
|
|
||||||
|
fun deleteDrafts(draftIds: LongArray): Flow<Boolean>
|
||||||
|
|
||||||
|
fun renameDraft(draftId: Long, name: String): Flow<Boolean>
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.isolaatti.audio.drafts.domain.use_case
|
||||||
|
|
||||||
|
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||||
|
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SaveAudioDraft @Inject constructor(private val audioDraftsRepository: AudioDraftsRepository) {
|
||||||
|
operator fun invoke(name: String, relativePath: String): Flow<AudioDraft> {
|
||||||
|
return audioDraftsRepository.saveAudioDraft(name, relativePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ import androidx.media3.common.MediaItem
|
|||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import com.isolaatti.audio.common.domain.Audio
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
|
import com.isolaatti.audio.common.domain.Playable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -28,7 +29,7 @@ class AudioPlayerConnector(
|
|||||||
const val TAG = "AudioPlayerConnector"
|
const val TAG = "AudioPlayerConnector"
|
||||||
}
|
}
|
||||||
private var player: Player? = null
|
private var player: Player? = null
|
||||||
private var audio: Audio? = null
|
private var audio: Playable? = null
|
||||||
private var mediaItem: MediaItem? = null
|
private var mediaItem: MediaItem? = null
|
||||||
private var ended = false
|
private var ended = false
|
||||||
|
|
||||||
@ -83,12 +84,23 @@ class AudioPlayerConnector(
|
|||||||
Player.STATE_ENDED -> {
|
Player.STATE_ENDED -> {
|
||||||
Log.d(TAG, "STATE_ENDED")
|
Log.d(TAG, "STATE_ENDED")
|
||||||
audio?.let {
|
audio?.let {
|
||||||
listeners.forEach { listener -> listener.onPlaying(false, it)}
|
listeners.forEach { listener ->
|
||||||
|
listener.onPlaying(false, it)
|
||||||
|
listener.onEnded(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
stopTimer()
|
stopTimer()
|
||||||
ended = true
|
ended = true
|
||||||
}
|
}
|
||||||
Player.STATE_BUFFERING -> {}
|
Player.STATE_BUFFERING -> {
|
||||||
|
player?.totalBufferedDuration?.let {
|
||||||
|
val seconds = (it / 1000).toInt()
|
||||||
|
Log.d(TAG, "Duration $it")
|
||||||
|
audio?.let {
|
||||||
|
listeners.forEach { listener -> listener.durationChanged(seconds, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Player.STATE_IDLE -> {}
|
Player.STATE_IDLE -> {}
|
||||||
Player.STATE_READY -> {
|
Player.STATE_READY -> {
|
||||||
Log.d(TAG, "STATE_READY")
|
Log.d(TAG, "STATE_READY")
|
||||||
@ -136,12 +148,13 @@ class AudioPlayerConnector(
|
|||||||
}
|
}
|
||||||
Lifecycle.Event.ON_STOP, Lifecycle.Event.ON_DESTROY -> {
|
Lifecycle.Event.ON_STOP, Lifecycle.Event.ON_DESTROY -> {
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
|
listeners.clear()
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playPauseAudio(audio: Audio) {
|
fun playPauseAudio(audio: Playable) {
|
||||||
|
|
||||||
// intention is to pause current audio
|
// intention is to pause current audio
|
||||||
if(audio == this.audio && player?.isPlaying == true) {
|
if(audio == this.audio && player?.isPlaying == true) {
|
||||||
@ -162,10 +175,36 @@ class AudioPlayerConnector(
|
|||||||
player?.setMediaItem(mediaItem!!)
|
player?.setMediaItem(mediaItem!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun stopPlayback() {
|
||||||
|
ended = true
|
||||||
|
player?.pause()
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onPlaying(isPlaying: Boolean, audio: Audio)
|
fun onPlaying(isPlaying: Boolean, audio: Playable)
|
||||||
fun isLoading(isLoading: Boolean, audio: Audio)
|
fun isLoading(isLoading: Boolean, audio: Playable)
|
||||||
fun progressChanged(second: Int, audio: Audio)
|
fun progressChanged(second: Int, audio: Playable)
|
||||||
fun durationChanged(duration: Int, audio: Audio)
|
fun durationChanged(duration: Int, audio: Playable)
|
||||||
|
fun onEnded(audio: Playable)
|
||||||
|
}
|
||||||
|
|
||||||
|
open class DefaultListener() : Listener {
|
||||||
|
override fun onPlaying(isPlaying: Boolean, audio: Playable) {}
|
||||||
|
|
||||||
|
override fun isLoading(isLoading: Boolean, audio: Playable) {}
|
||||||
|
|
||||||
|
override fun progressChanged(second: Int, audio: Playable) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun durationChanged(duration: Int, audio: Playable) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnded(audio: Playable) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.isolaatti.audio.recorder.presentation
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||||
|
import com.isolaatti.audio.drafts.domain.use_case.SaveAudioDraft
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class AudioRecorderViewModel @Inject constructor(
|
||||||
|
private val saveAudioDraftUC: SaveAudioDraft
|
||||||
|
) : ViewModel() {
|
||||||
|
var name: String = ""
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
var relativePath = ""
|
||||||
|
private var audioRecorded = false
|
||||||
|
|
||||||
|
val audioDraft: MutableLiveData<AudioDraft> = MutableLiveData()
|
||||||
|
|
||||||
|
val canSave: MutableLiveData<Boolean> = MutableLiveData()
|
||||||
|
|
||||||
|
private fun validate() {
|
||||||
|
canSave.value = audioRecorded && name.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAudioRecorded() {
|
||||||
|
audioRecorded = true
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClearAudio() {
|
||||||
|
audioRecorded = false
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
fun saveAudioDraft() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
saveAudioDraftUC(name, relativePath).onEach {
|
||||||
|
audioDraft.postValue(it)
|
||||||
|
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.isolaatti.audio.recorder.presentation
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.isolaatti.audio.drafts.ui.AudioDraftsFragment
|
||||||
|
import com.isolaatti.audio.recorder.ui.AudioRecorderHelperNotesFragment
|
||||||
|
|
||||||
|
class RecorderPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return when (position) {
|
||||||
|
0 -> AudioRecorderHelperNotesFragment()
|
||||||
|
1 -> AudioDraftsFragment()
|
||||||
|
else -> Fragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,18 +2,36 @@ package com.isolaatti.audio.recorder.ui
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.content.res.Resources.Theme
|
||||||
|
import android.media.AudioManager
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.PermissionChecker
|
import androidx.core.content.PermissionChecker
|
||||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.get
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.dialog.MaterialDialogs
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import com.isolaatti.R
|
||||||
|
import com.isolaatti.audio.common.domain.Playable
|
||||||
|
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||||
|
import com.isolaatti.audio.player.AudioPlayerConnector
|
||||||
|
import com.isolaatti.audio.recorder.presentation.AudioRecorderViewModel
|
||||||
|
import com.isolaatti.audio.recorder.presentation.RecorderPagerAdapter
|
||||||
import com.isolaatti.databinding.ActivityAudioRecorderBinding
|
import com.isolaatti.databinding.ActivityAudioRecorderBinding
|
||||||
|
import com.isolaatti.utils.Resource
|
||||||
|
import com.isolaatti.utils.clockFormat
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -29,13 +47,18 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val LOG_TAG = "AudioRecorderActivity"
|
const val LOG_TAG = "AudioRecorderActivity"
|
||||||
|
|
||||||
|
const val IN_EXTRA_DRAFT_ID = "in_draft_id"
|
||||||
|
const val OUT_EXTRA_DRAFT_ID = "out_draft_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var binding: ActivityAudioRecorderBinding
|
private lateinit var binding: ActivityAudioRecorderBinding
|
||||||
|
|
||||||
private var audioRecorder: MediaRecorder? = null
|
private var audioRecorder: MediaRecorder? = null
|
||||||
|
private var recordingPaused = false
|
||||||
|
private var audioPlayerConnector: AudioPlayerConnector? = null
|
||||||
|
|
||||||
private val audioUID = UUID.randomUUID()
|
private val viewModel: AudioRecorderViewModel by viewModels()
|
||||||
private lateinit var outputFile: String
|
private lateinit var outputFile: String
|
||||||
|
|
||||||
private val requestAudioPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
private val requestAudioPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
@ -46,20 +69,59 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val audioPlayerListener = object: AudioPlayerConnector.DefaultListener() {
|
||||||
|
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
|
||||||
|
if(isPlaying) {
|
||||||
|
binding.seekbar.isEnabled = true
|
||||||
|
binding.playPauseButton.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
|
||||||
|
} else {
|
||||||
|
binding.playPauseButton.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_play_arrow_24, theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnded(audio: Playable) {
|
||||||
|
binding.seekbar.isEnabled = false
|
||||||
|
binding.seekbar.progress = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun durationChanged(duration: Int, audio: Playable) {
|
||||||
|
super.durationChanged(duration, audio)
|
||||||
|
binding.seekbar.max = duration
|
||||||
|
totalTime = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun progressChanged(second: Int, audio: Playable) {
|
||||||
|
binding.seekbar.progress = second
|
||||||
|
setDisplayTime(second, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
binding = ActivityAudioRecorderBinding.inflate(layoutInflater)
|
binding = ActivityAudioRecorderBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
// Path where results are stored
|
||||||
File("${filesDir.absolutePath}/audios/").let {
|
File("${filesDir.absolutePath}/audios/").let {
|
||||||
if(!it.isDirectory) {
|
if(!it.isDirectory) {
|
||||||
it.mkdir()
|
it.mkdir()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outputFile = "${filesDir.absolutePath}/audios/${audioUID}.3gp"
|
|
||||||
|
binding.seekbar.isEnabled = false
|
||||||
|
outputFile = "${cacheDir.absolutePath}/audio_recorder.3gp"
|
||||||
|
|
||||||
setupListeners()
|
setupListeners()
|
||||||
|
setupObservers()
|
||||||
|
|
||||||
|
audioPlayerConnector = AudioPlayerConnector(this)
|
||||||
|
audioPlayerConnector?.addListener(audioPlayerListener)
|
||||||
|
|
||||||
|
lifecycle.addObserver(audioPlayerConnector!!)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkRecordAudioPermission(): Boolean {
|
private fun checkRecordAudioPermission(): Boolean {
|
||||||
@ -112,25 +174,60 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
pauseRecording()
|
pauseRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.acceptButton.setOnClickListener {
|
binding.playPauseButton.setOnClickListener {
|
||||||
acceptRecording()
|
playPauseRecording(object: Playable() {
|
||||||
|
override val uri: Uri
|
||||||
|
get() = outputFile.toUri()
|
||||||
|
override val thumbnail: String?
|
||||||
|
get() = null
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playPauseButton.setOnClickListener {
|
binding.inputDraftName.editText?.doOnTextChanged { text, _, _, _ ->
|
||||||
|
viewModel.name = text.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbar.setOnMenuItemClickListener {
|
||||||
|
when(it.itemId) {
|
||||||
|
R.id.save_draft -> {
|
||||||
|
viewModel.saveAudioDraft()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupObservers() {
|
||||||
|
viewModel.canSave.observe(this) {
|
||||||
|
binding.toolbar.menu.findItem(R.id.save_draft).isEnabled = it
|
||||||
|
}
|
||||||
|
|
||||||
|
// audio draft is saved!
|
||||||
|
viewModel.audioDraft.observe(this) {
|
||||||
|
val result = Intent().apply {
|
||||||
|
putExtra(OUT_EXTRA_DRAFT_ID, it.id)
|
||||||
|
}
|
||||||
|
setResult(RESULT_OK, result)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// region timer
|
// region timer
|
||||||
private var timer: Job? = null
|
private var timer: Job? = null
|
||||||
private var timerValue = 0
|
private var timerValue = 0
|
||||||
|
private var totalTime = 0
|
||||||
|
|
||||||
private fun setDisplayTime(seconds: Int, showTotalTime: Boolean) {
|
private fun setDisplayTime(seconds: Int, showTotalTime: Boolean) {
|
||||||
val stringBuilder = StringBuilder()
|
binding.time.text = buildString {
|
||||||
|
append(seconds.clockFormat())
|
||||||
|
|
||||||
stringBuilder.append(seconds)
|
if(showTotalTime) {
|
||||||
|
append("/")
|
||||||
binding.time.text = stringBuilder.toString()
|
append(totalTime.clockFormat())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private fun startTimerRecorder() {
|
private fun startTimerRecorder() {
|
||||||
|
|
||||||
@ -142,14 +239,6 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startTimerPlayer() {
|
|
||||||
timer?.cancel()
|
|
||||||
timer = CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
setDisplayTime(timerValue, true)
|
|
||||||
delay(1000)
|
|
||||||
timerValue++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopTimer() {
|
private fun stopTimer() {
|
||||||
timer?.cancel()
|
timer?.cancel()
|
||||||
@ -159,6 +248,8 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// region record functions
|
// region record functions
|
||||||
private fun startRecording() {
|
private fun startRecording() {
|
||||||
|
viewModel.onClearAudio()
|
||||||
|
recordingPaused = false
|
||||||
audioRecorder = MediaRecorder().apply {
|
audioRecorder = MediaRecorder().apply {
|
||||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||||
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
|
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
|
||||||
@ -171,6 +262,7 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
timerValue = 0
|
timerValue = 0
|
||||||
startTimerRecorder()
|
startTimerRecorder()
|
||||||
binding.viewAnimator.displayedChild = 1
|
binding.viewAnimator.displayedChild = 1
|
||||||
|
|
||||||
} catch(e: IOException) {
|
} catch(e: IOException) {
|
||||||
Log.e(LOG_TAG, "prepare() failed\n${e.message}")
|
Log.e(LOG_TAG, "prepare() failed\n${e.message}")
|
||||||
}
|
}
|
||||||
@ -187,12 +279,26 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// shows third state: audio recorded
|
// shows third state: audio recorded
|
||||||
binding.viewAnimator.displayedChild = 2
|
binding.viewAnimator.displayedChild = 2
|
||||||
|
recordingPaused = false
|
||||||
|
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
|
||||||
|
binding.pauseRecording.setIconTintResource(com.google.android.material.R.color.m3_icon_button_icon_color_selector)
|
||||||
|
viewModel.onAudioRecorded()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pauseRecording() {
|
private fun pauseRecording() {
|
||||||
|
if(recordingPaused) {
|
||||||
|
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
|
||||||
|
binding.pauseRecording.setIconTintResource(com.google.android.material.R.color.m3_icon_button_icon_color_selector)
|
||||||
|
audioRecorder?.resume()
|
||||||
|
startTimerRecorder()
|
||||||
|
} else {
|
||||||
|
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_circle_24, theme)
|
||||||
|
binding.pauseRecording.setIconTintResource(R.color.danger)
|
||||||
audioRecorder?.pause()
|
audioRecorder?.pause()
|
||||||
stopTimer()
|
stopTimer()
|
||||||
}
|
}
|
||||||
|
recordingPaused = !recordingPaused
|
||||||
|
}
|
||||||
|
|
||||||
private fun discardRecording(){
|
private fun discardRecording(){
|
||||||
File(outputFile).apply {
|
File(outputFile).apply {
|
||||||
@ -203,16 +309,17 @@ class AudioRecorderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.viewAnimator.displayedChild = 0
|
binding.viewAnimator.displayedChild = 0
|
||||||
|
viewModel.onClearAudio()
|
||||||
|
totalTime = 0
|
||||||
|
timerValue = 0
|
||||||
|
binding.time.text = ""
|
||||||
|
audioPlayerConnector?.stopPlayback()
|
||||||
}
|
}
|
||||||
|
|
||||||
// end region
|
// end region
|
||||||
|
|
||||||
private fun acceptRecording() {
|
private fun playPauseRecording(audioDraft: Playable) {
|
||||||
|
audioPlayerConnector?.playPauseAudio(audioDraft)
|
||||||
}
|
|
||||||
|
|
||||||
private fun playPauseRecording() {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.isolaatti.audio.recorder.ui
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
|
||||||
|
class AudioRecorderContract : ActivityResultContract<Long?, Long?>() {
|
||||||
|
|
||||||
|
override fun createIntent(context: Context, input: Long?): Intent {
|
||||||
|
val intent = Intent(context, AudioRecorderActivity::class.java).apply {
|
||||||
|
if(input != null) {
|
||||||
|
putExtra(AudioRecorderActivity.IN_EXTRA_DRAFT_ID, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(resultCode: Int, intent: Intent?): Long? {
|
||||||
|
return when(resultCode){
|
||||||
|
Activity.RESULT_OK -> {
|
||||||
|
intent?.getLongExtra(AudioRecorderActivity.OUT_EXTRA_DRAFT_ID, 0)?.takeUnless { it == 0L }
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.isolaatti.audio.recorder.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
class AudioRecorderHelperNotesFragment : Fragment() {
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return LinearLayout(requireContext()).apply {
|
||||||
|
addView(TextView(requireContext()).apply {
|
||||||
|
text = "Allow the user to place some info here to be " +
|
||||||
|
"reading while recording. If this screen is launched from comments, show here the post"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ import com.isolaatti.auth.data.local.UserInfoEntity
|
|||||||
import com.isolaatti.settings.data.KeyValueDao
|
import com.isolaatti.settings.data.KeyValueDao
|
||||||
import com.isolaatti.settings.data.KeyValueEntity
|
import com.isolaatti.settings.data.KeyValueEntity
|
||||||
|
|
||||||
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class], version = 3)
|
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class], version = 4)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun keyValueDao(): KeyValueDao
|
abstract fun keyValueDao(): KeyValueDao
|
||||||
abstract fun userInfoDao(): UserInfoDao
|
abstract fun userInfoDao(): UserInfoDao
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import com.isolaatti.audio.recorder.ui.AudioRecorderActivity
|
import com.isolaatti.audio.recorder.ui.AudioRecorderActivity
|
||||||
|
import com.isolaatti.audio.recorder.ui.AudioRecorderContract
|
||||||
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
|
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
|
||||||
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
|
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
|
||||||
import com.isolaatti.posting.link_creator.presentation.LinkCreatorViewModel
|
import com.isolaatti.posting.link_creator.presentation.LinkCreatorViewModel
|
||||||
@ -22,6 +23,12 @@ class MarkdownEditingFragment : Fragment(){
|
|||||||
private val viewModel: CreatePostViewModel by activityViewModels()
|
private val viewModel: CreatePostViewModel by activityViewModels()
|
||||||
private val linkCreatorViewModel: LinkCreatorViewModel by viewModels()
|
private val linkCreatorViewModel: LinkCreatorViewModel by viewModels()
|
||||||
|
|
||||||
|
private val audioRecorderLauncher = registerForActivityResult(AudioRecorderContract()) { draftId ->
|
||||||
|
if(draftId != null) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val imageChooserLauncher = registerForActivityResult(ImageChooserContract()) { image ->
|
private val imageChooserLauncher = registerForActivityResult(ImageChooserContract()) { image ->
|
||||||
Log.d("MarkdownEditingFragment", "${image?.markdown}")
|
Log.d("MarkdownEditingFragment", "${image?.markdown}")
|
||||||
|
|
||||||
@ -67,7 +74,7 @@ class MarkdownEditingFragment : Fragment(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.addAudioButton.setOnClickListener {
|
binding.addAudioButton.setOnClickListener {
|
||||||
requireContext().startActivity(Intent(requireContext(), AudioRecorderActivity::class.java))
|
audioRecorderLauncher.launch(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import com.isolaatti.BuildConfig
|
|||||||
import com.isolaatti.R
|
import com.isolaatti.R
|
||||||
import com.isolaatti.audio.audios_list.ui.AudiosFragment
|
import com.isolaatti.audio.audios_list.ui.AudiosFragment
|
||||||
import com.isolaatti.audio.common.domain.Audio
|
import com.isolaatti.audio.common.domain.Audio
|
||||||
|
import com.isolaatti.audio.common.domain.Playable
|
||||||
import com.isolaatti.audio.player.AudioPlayerConnector
|
import com.isolaatti.audio.player.AudioPlayerConnector
|
||||||
import com.isolaatti.common.CoilImageLoader.imageLoader
|
import com.isolaatti.common.CoilImageLoader.imageLoader
|
||||||
import com.isolaatti.common.Dialogs
|
import com.isolaatti.common.Dialogs
|
||||||
@ -106,24 +107,28 @@ class ProfileMainFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
|
private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
|
||||||
override fun onPlaying(isPlaying: Boolean, audio: Audio) {
|
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
|
||||||
viewBinding.playButton.icon = AppCompatResources.getDrawable(requireContext(), if(isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24)
|
viewBinding.playButton.icon = AppCompatResources.getDrawable(requireContext(), if(isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isLoading(isLoading: Boolean, audio: Audio) {
|
override fun isLoading(isLoading: Boolean, audio: Playable) {
|
||||||
viewBinding.playButton.isEnabled = !isLoading
|
viewBinding.playButton.isEnabled = !isLoading
|
||||||
viewBinding.audioProgress.isIndeterminate = isLoading
|
viewBinding.audioProgress.isIndeterminate = isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun progressChanged(second: Int, audio: Audio) {
|
override fun progressChanged(second: Int, audio: Playable) {
|
||||||
viewBinding.audioProgress.setProgress(second, true)
|
viewBinding.audioProgress.setProgress(second, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun durationChanged(duration: Int, audio: Audio) {
|
override fun durationChanged(duration: Int, audio: Playable) {
|
||||||
viewBinding.audioProgress.max = duration
|
viewBinding.audioProgress.max = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onEnded(audio: Playable) {
|
||||||
|
viewBinding.audioProgress.progress = 0
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val profileObserver = Observer<UserProfile> { profile ->
|
private val profileObserver = Observer<UserProfile> { profile ->
|
||||||
|
|||||||
8
app/src/main/java/com/isolaatti/utils/Extensions.kt
Normal file
8
app/src/main/java/com/isolaatti/utils/Extensions.kt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package com.isolaatti.utils
|
||||||
|
|
||||||
|
fun Int.clockFormat(): String {
|
||||||
|
val minutes = this / 60
|
||||||
|
val seconds = this % 60
|
||||||
|
|
||||||
|
return "${minutes}:${seconds.toString().padStart(2, '0')}"
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
@ -12,23 +12,41 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:navigationIcon="@drawable/baseline_close_24"/>
|
app:title="@string/audio_recorder"
|
||||||
|
app:navigationIcon="@drawable/baseline_close_24"
|
||||||
|
app:menu="@menu/audio_recorder_menu"/>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/input_draft_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
|
android:hint="Name"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/materialCardView2"
|
||||||
|
style="?attr/materialCardViewElevatedStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/time"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
|
||||||
android:layout_margin="24dp"
|
android:layout_margin="24dp"
|
||||||
style="?attr/materialCardViewFilledStyle">
|
app:layout_constraintBottom_toTopOf="@id/time"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/input_draft_name"
|
||||||
|
tools:layout_editor_absoluteX="24dp">
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/fragment_container"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
app:navGraph="@navigation/audio_recorder_navigation"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
app:defaultNavHost="true"/>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
@ -36,22 +54,22 @@
|
|||||||
android:id="@+id/time"
|
android:id="@+id/time"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toTopOf="@id/card_controls"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
|
android:textAlignment="center"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
tools:text="00:00"
|
app:layout_constraintBottom_toTopOf="@id/card_controls"
|
||||||
android:textAlignment="center"/>
|
tools:text="00:00" />
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/card_controls"
|
android:id="@+id/card_controls"
|
||||||
|
style="?attr/materialCardViewFilledStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="24dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
style="?attr/materialCardViewFilledStyle"
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
android:layout_margin="24dp">
|
|
||||||
|
|
||||||
<ViewAnimator
|
<ViewAnimator
|
||||||
android:id="@+id/viewAnimator"
|
android:id="@+id/viewAnimator"
|
||||||
@ -115,16 +133,25 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_margin="8dp">
|
android:layout_margin="8dp">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/cancel_button"
|
android:id="@+id/cancel_button"
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:icon="@drawable/baseline_close_24"
|
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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/seekbar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
|
app:layout_constraintStart_toEndOf="@id/cancel_button"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/play_pause_button"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/play_pause_button"
|
android:id="@+id/play_pause_button"
|
||||||
@ -132,26 +159,12 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:icon="@drawable/baseline_play_arrow_24"
|
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_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
app:layout_constraintStart_toEndOf="@+id/play_pause_button"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</ViewAnimator>
|
</ViewAnimator>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@ -1,7 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<TextView
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Audio drafts"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler"
|
android:id="@+id/recycler"
|
||||||
|
|||||||
9
app/src/main/res/menu/audio_recorder_menu.xml
Normal file
9
app/src/main/res/menu/audio_recorder_menu.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/save_draft"
|
||||||
|
android:title="Save"
|
||||||
|
app:showAsAction="always"
|
||||||
|
android:enabled="false"/>
|
||||||
|
</menu>
|
||||||
@ -1,7 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/audio_recorder_navigation">
|
android:id="@+id/audio_recorder_navigation"
|
||||||
|
app:startDestination="@id/audioDraftsFragment">
|
||||||
|
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/audioDraftsFragment"
|
||||||
|
android:name="com.isolaatti.audio.drafts.ui.AudioDraftsFragment"
|
||||||
|
android:label="AudioDraftsFragment" />
|
||||||
</navigation>
|
</navigation>
|
||||||
Loading…
x
Reference in New Issue
Block a user