diff --git a/app/build.gradle b/app/build.gradle index 1aec3db..b6df519 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -159,7 +159,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5' implementation 'androidx.compose.runtime:runtime-livedata' - - implementation("com.google.accompanist:accompanist-permissions:0.36.0") + + implementation "androidx.work:work-runtime-ktx:2.10.0" } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/audio/common/components/AudioPlayer.kt b/app/src/main/java/com/isolaatti/audio/common/components/AudioPlayer.kt index 5790690..574fa45 100644 --- a/app/src/main/java/com/isolaatti/audio/common/components/AudioPlayer.kt +++ b/app/src/main/java/com/isolaatti/audio/common/components/AudioPlayer.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -48,7 +49,7 @@ fun AudioPlayer( Card(modifier = modifier) { - Row(modifier = Modifier.padding(16.dp)) { + Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { IconButton(onClick = {if(isPlaying) onPause() else onPlay()}) { if(isPlaying) { Icon(painterResource(R.drawable.baseline_pause_24), null) diff --git a/app/src/main/java/com/isolaatti/audio/common/components/AudioRecorder.kt b/app/src/main/java/com/isolaatti/audio/common/components/AudioRecorder.kt index d7d37a8..364b186 100644 --- a/app/src/main/java/com/isolaatti/audio/common/components/AudioRecorder.kt +++ b/app/src/main/java/com/isolaatti/audio/common/components/AudioRecorder.kt @@ -19,8 +19,10 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -40,6 +42,35 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.milliseconds + +class AudioRecorderState( + isRecording: Boolean = false, + recordIsStopped: Boolean = false, + recordIsPaused: Boolean = false, + isPlaying: Boolean = false, + isLoading: Boolean = false, + durationSeconds: Long = 0L, + position: Long = 0L, + +) { + var isRecording by mutableStateOf(isRecording) + var recordIsStopped by mutableStateOf(recordIsStopped) + var recordIsPaused by mutableStateOf(recordIsPaused) + var isPlaying by mutableStateOf(isPlaying) + var isLoading by mutableStateOf(isLoading) + var durationSeconds by mutableStateOf(durationSeconds) + var position by mutableStateOf(position) + + fun clearState() { + recordIsStopped = false + isRecording = false + recordIsPaused = false + isPlaying = false + durationSeconds = 0L + position = 0L + } +} + @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun AudioRecorder( @@ -49,13 +80,7 @@ fun AudioRecorder( onStopRecording: () -> Unit, onPauseRecording: () -> Unit, onStartRecording: (fromPaused: Boolean) -> Unit, - isRecording: Boolean, - recordIsStopped: Boolean, - recordIsPaused: Boolean, - isPlaying: Boolean, - isLoading: Boolean, - durationSeconds: Long, - position: Long, + state: AudioRecorderState = AudioRecorderState(), onSeek: (position: Float) -> Unit = {}, modifier: Modifier = Modifier ) { @@ -68,9 +93,9 @@ fun AudioRecorder( var duration by remember { mutableLongStateOf(0L) } - LaunchedEffect(isRecording, recordIsPaused) { + LaunchedEffect(state.isRecording, state.recordIsPaused) { coroutineScope { - while(isRecording && !recordIsPaused) { + while(state.isRecording && !state.recordIsPaused) { duration += 500 delay(500.milliseconds) } @@ -94,7 +119,7 @@ fun AudioRecorder( when { audioRecordPermissionState.status.isGranted -> { when { - !isRecording && !recordIsStopped-> { + !state.isRecording && !state.recordIsStopped-> { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -113,7 +138,7 @@ fun AudioRecorder( } } - isRecording && !recordIsStopped -> { + state.isRecording && !state.recordIsStopped -> { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 8.dp) @@ -121,7 +146,7 @@ fun AudioRecorder( Text(duration.milliseconds.clockFormat(), modifier = Modifier.padding(horizontal = 4.dp)) Spacer(Modifier.weight(1f)) IconButton(onClick = { - if(recordIsPaused) { + if(state.recordIsPaused) { onStartRecording(true) } else { @@ -129,7 +154,7 @@ fun AudioRecorder( } }) { - if(recordIsPaused) { + if(state.recordIsPaused) { Icon(painterResource(R.drawable.baseline_circle_24), null, tint = Color.Red) } else { Icon(painterResource(R.drawable.baseline_pause_24), null) @@ -146,10 +171,10 @@ fun AudioRecorder( else -> { AudioPlayer( modifier = Modifier.fillMaxWidth(), - positionMs = position, - isLoading = isLoading, - isPlaying = isPlaying, - durationMs = durationSeconds, + positionMs = state.position, + isLoading = state.isLoading, + isPlaying = state.isPlaying, + durationMs = state.durationSeconds, onPlay = onPlay, onPause = onPause, onSeek = onSeek diff --git a/app/src/main/java/com/isolaatti/audio/player/AudioPlaybackState.kt b/app/src/main/java/com/isolaatti/audio/player/AudioPlaybackState.kt new file mode 100644 index 0000000..5f41360 --- /dev/null +++ b/app/src/main/java/com/isolaatti/audio/player/AudioPlaybackState.kt @@ -0,0 +1,10 @@ +package com.isolaatti.audio.player + +import com.isolaatti.audio.common.domain.Audio + +data class AudioPlaybackState( + val currentAudio: Audio? = null, + var isPlaying: Boolean = false, + val duration: Long = 0L, + val progress: Long = 0L +) diff --git a/app/src/main/java/com/isolaatti/posting/posts/components/PostComponent.kt b/app/src/main/java/com/isolaatti/posting/posts/components/PostComponent.kt index 5ce2a5d..6e096bf 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/components/PostComponent.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/components/PostComponent.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -33,10 +34,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.isolaatti.R +import com.isolaatti.audio.common.components.AudioPlayer +import com.isolaatti.audio.common.domain.Audio +import com.isolaatti.audio.player.AudioPlaybackState import com.isolaatti.images.common.domain.entity.RemoteImage import com.isolaatti.posting.posts.domain.entity.Post import com.isolaatti.utils.UrlGen.userProfileImage +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostComponent( post: Post, @@ -50,6 +55,7 @@ fun PostComponent( onUsernameClick: () -> Unit = {}, onImageClick: (images: List, index: Int) -> Unit = {_, _ -> }, onHashtagClick: (hashtag: String) -> Unit = {}, + onPlay: (audio: Audio, positionMs: Long) -> Unit = {_, _ -> }, modifier: Modifier = Modifier ) { Card(modifier = modifier.padding(8.dp).clickable { onClick() }) { @@ -77,7 +83,22 @@ fun PostComponent( } post.audio?.let { - // TODO audio player here + AudioPlayer( + modifier = Modifier.fillMaxWidth().height(80.dp), + positionMs = 2000L, + isPlaying = false, + durationMs = 30_000L, + isLoading = false, + onPlay = { + + }, + onPause = { + + }, + onSeek = { + + } + ) } if(post.textContent.isNotBlank()) { @@ -167,5 +188,5 @@ fun PostComponent( @Composable @Preview(device = Devices.PIXEL_5) fun PostPreview() { - PostComponent(Post(id = 0L, textContent = "Test", userId = 1, privacy = 2, date = "Date", images = emptyList(), liked = false, userName = "Username", numberOfLikes = 1, numberOfComments = 2)) + PostComponent(Post(id = 0L, textContent = "Test", userId = 1, privacy = 2, date = "Date", images = emptyList(), liked = false, userName = "Username", numberOfLikes = 2, numberOfComments = 3)) } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/domain/entity/Post.kt b/app/src/main/java/com/isolaatti/posting/posts/domain/entity/Post.kt index abd06fb..63d6fc1 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/domain/entity/Post.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/domain/entity/Post.kt @@ -4,6 +4,7 @@ import android.os.Parcel import android.os.Parcelable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.isolaatti.audio.common.domain.Audio @@ -35,6 +36,10 @@ class Post( var numberOfLikes by mutableIntStateOf(numberOfLikes) var numberOfComments by mutableStateOf(numberOfComments) + var playing by mutableStateOf(false) + var durationMs by mutableLongStateOf(0L) + var progressMs by mutableLongStateOf(0L) + constructor(parcel: Parcel) : this( parcel.readLong(), parcel.readString()!!, diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/CreatePostViewModel.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/CreatePostViewModel.kt index 149be0c..c319655 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/CreatePostViewModel.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/CreatePostViewModel.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.isolaatti.audio.common.components.AudioRecorderState import com.isolaatti.audio.common.domain.Audio import com.isolaatti.audio.common.domain.Playable import com.isolaatti.images.common.domain.entity.Image @@ -61,19 +62,9 @@ class CreatePostViewModel @Inject constructor( private var audioDraft: Long? = null private var audioId: String? = null - // region Player state - val isPlaying: MutableStateFlow = MutableStateFlow(false) - var position: MutableStateFlow = MutableStateFlow(0L) - var duration: MutableStateFlow = MutableStateFlow(0L) - var isLoading: MutableStateFlow = MutableStateFlow(false) + val audioRecorderState = AudioRecorderState() var ended: Boolean = false - // endregion - // region Recorder state - var isRecording: MutableStateFlow = MutableStateFlow(false) - var recordingIsPaused: MutableStateFlow = MutableStateFlow(false) - var recodingIsStopped: MutableStateFlow = MutableStateFlow(false) - // endregion /** diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModelBase.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModelBase.kt index aff50e9..987f0d4 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModelBase.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModelBase.kt @@ -4,6 +4,9 @@ import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.isolaatti.audio.common.components.AudioRecorderState +import com.isolaatti.audio.common.domain.Audio +import com.isolaatti.audio.player.AudioPlaybackState import com.isolaatti.posting.likes.domain.repository.LikesRepository import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.domain.entity.Post @@ -40,8 +43,14 @@ abstract class PostListingViewModelBase : ViewModel() { val errorLoading: MutableLiveData = MutableLiveData() var isLoadingFromScrolling = false + val mediaControllerIsAvailable: MutableStateFlow = MutableStateFlow(false) + + val audioState: MutableStateFlow = MutableStateFlow(AudioPlaybackState()) + fun getLastId(): Long = try { posts.value?.last()?.id ?: 0 } catch (e: NoSuchElementException) { 0 } + val audioRecorderState = AudioRecorderState() + abstract fun getFeed(refresh: Boolean, hashtag: String?) @@ -115,4 +124,16 @@ abstract class PostListingViewModelBase : ViewModel() { fun onPostAddedAtTheBeginning(post: Post) { posts.value = listOf(post) + posts.value } + + fun setAudioToPlay(audio: Audio) { + audioState.value = AudioPlaybackState(currentAudio = audio) + } + + fun setIsPlaying(isPlaying: Boolean) { + audioState.value = audioState.value.copy(isPlaying = isPlaying) + } + + fun updateProgress(progress: Long, duration: Long) { + audioState.value = audioState.value.copy(progress = progress, duration = duration) + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/ui/CreatePostActivity.kt b/app/src/main/java/com/isolaatti/posting/posts/ui/CreatePostActivity.kt index 9d01269..31a1d0e 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/ui/CreatePostActivity.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/ui/CreatePostActivity.kt @@ -124,21 +124,21 @@ class CreatePostActivity : IsolaattiBaseActivity() { private val playerListener: Player.Listener = object: Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { - viewModel.isPlaying.value = isPlaying + viewModel.audioRecorderState.isPlaying = isPlaying } override fun onPlaybackStateChanged(playbackState: Int) { when(playbackState) { Player.STATE_READY -> { viewModel.ended = false - viewModel.isLoading.value = false - viewModel.duration.value = this@CreatePostActivity.mediaController.duration + viewModel.audioRecorderState.isLoading = false + viewModel.audioRecorderState.durationSeconds = this@CreatePostActivity.mediaController.duration } Player.STATE_ENDED -> { viewModel.ended = true } Player.STATE_BUFFERING -> { - viewModel.isLoading.value = true + viewModel.audioRecorderState.isLoading = true } Player.STATE_IDLE -> {} @@ -291,19 +291,11 @@ class CreatePostActivity : IsolaattiBaseActivity() { AnimatedVisibility(audioRecorderIsVisible) { - val isPlaying by viewModel.isPlaying.collectAsState() - val position by viewModel.position.collectAsState() - val duration by viewModel.duration.collectAsState() - val isLoading by viewModel.isLoading.collectAsState() - val isRecording by viewModel.isRecording.collectAsState() - val recordIsPaused by viewModel.recordingIsPaused.collectAsState() - val recordIsStopped by viewModel.recodingIsStopped.collectAsState() - - LaunchedEffect(isPlaying) { - while(isPlaying) { + LaunchedEffect(viewModel.audioRecorderState.isPlaying) { + while(viewModel.audioRecorderState.isPlaying) { delay(500.milliseconds) - viewModel.position.value = mediaController.currentPosition + viewModel.audioRecorderState.position = mediaController.currentPosition } } @@ -327,12 +319,7 @@ class CreatePostActivity : IsolaattiBaseActivity() { mediaRecorder.release() // clear references - viewModel.recodingIsStopped.value = false - viewModel.isRecording.value = false - viewModel.recordingIsPaused.value = false - viewModel.isPlaying.value = false - viewModel.duration.value = 0L - viewModel.position.value = 0L + viewModel.audioRecorderState.clearState() viewModel.audioToUpload = null }, onPlay = { @@ -367,18 +354,19 @@ class CreatePostActivity : IsolaattiBaseActivity() { start() } } - viewModel.recodingIsStopped.value = false - viewModel.isRecording.value = true - viewModel.recordingIsPaused.value = false + + viewModel.audioRecorderState.recordIsStopped = false + viewModel.audioRecorderState.isRecording = true + viewModel.audioRecorderState.recordIsPaused = false }, onPauseRecording = { mediaRecorder.pause() - viewModel.recodingIsStopped.value = false - viewModel.isRecording.value = true - viewModel.recordingIsPaused.value = true + viewModel.audioRecorderState.recordIsStopped = false + viewModel.audioRecorderState.isRecording = true + viewModel.audioRecorderState.recordIsPaused = true }, onStopRecording = { mediaRecorder.stop() @@ -392,17 +380,11 @@ class CreatePostActivity : IsolaattiBaseActivity() { .build() ) viewModel.audioToUpload = audioUri - viewModel.recodingIsStopped.value = true - viewModel.isRecording.value = false - viewModel.recordingIsPaused.value = false + viewModel.audioRecorderState.recordIsStopped = true + viewModel.audioRecorderState.isRecording = false + viewModel.audioRecorderState.recordIsPaused = false }, - isPlaying = isPlaying, - position = position, - durationSeconds = duration, - isRecording = isRecording, - recordIsPaused = recordIsPaused, - recordIsStopped = recordIsStopped, - isLoading = isLoading, + state = viewModel.audioRecorderState, onSeek = { ms -> mediaController.seekTo(ms.toLong()) } diff --git a/app/src/main/java/com/isolaatti/posting/posts/ui/PostListingFragment.kt b/app/src/main/java/com/isolaatti/posting/posts/ui/PostListingFragment.kt index 74a10e3..c9003ff 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/ui/PostListingFragment.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/ui/PostListingFragment.kt @@ -26,6 +26,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import com.isolaatti.BuildConfig import com.isolaatti.R +import com.isolaatti.audio.common.components.AudioRecorderState import com.isolaatti.audio.common.domain.Audio import com.isolaatti.common.Dialogs import com.isolaatti.common.ErrorMessageViewModel @@ -63,6 +64,7 @@ class PostListingFragment : Fragment() { private val viewModel: PostListingViewModel by viewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(LOG_TAG, "onCreate ; hashtag: $hashtag") diff --git a/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt b/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt index e361430..9927492 100644 --- a/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt +++ b/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt @@ -57,6 +57,8 @@ class ProfileViewModel @Inject constructor( val uploadingProfilePicture: MutableStateFlow = MutableStateFlow(false) val errorProfilePicture: MutableStateFlow?> = MutableStateFlow(null) + val settingUpAudio: MutableStateFlow = MutableStateFlow(false) + // runs the lists of "Runnable" one by one and clears list. After this is executed, // caller should report as handled diff --git a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt index 6c69a2b..1453871 100644 --- a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt +++ b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt @@ -3,6 +3,8 @@ package com.isolaatti.profile.ui import android.content.ComponentName import android.content.Intent import android.media.MediaRecorder +import android.os.Build +import android.media.MediaRecorder import android.net.Uri import android.os.Build import android.os.Bundle @@ -16,14 +18,18 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -31,6 +37,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.rememberModalBottomSheetState @@ -43,6 +50,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -70,7 +78,9 @@ import com.google.common.util.concurrent.ListenableFuture import com.isolaatti.BuildConfig import com.isolaatti.MyApplication import com.isolaatti.R +import com.isolaatti.audio.common.components.AudioPlayer import com.isolaatti.audio.common.components.AudioRecorder +import com.isolaatti.audio.common.domain.Audio import com.isolaatti.audio.player.MediaService import com.isolaatti.common.Dialogs import com.isolaatti.common.ErrorMessageViewModel @@ -100,8 +110,11 @@ import com.isolaatti.profile.ui.components.ProfilePictureState import com.isolaatti.reports.data.ContentType import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.guava.await import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.util.Calendar @@ -159,9 +172,12 @@ class ProfileMainFragment : Fragment() { private lateinit var mediaController: MediaController private lateinit var mediaRecorder: MediaRecorder + private var mediaRecorder: MediaRecorder? = null + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + viewModel.mediaControllerIsAvailable.value = false val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), MediaService::class.java)) mediaControllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync() @@ -169,6 +185,58 @@ class ProfileMainFragment : Fragment() { getData() } + private lateinit var recordOutputFile: File + + // initialized in onViewCreated + private lateinit var directory: File + + private fun startRecording(callback: () -> Unit) { + recordOutputFile = File(directory, "${System.currentTimeMillis()}.3gp") + mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(requireContext()) else MediaRecorder() + mediaRecorder?.run { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) + setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + if (Build.VERSION.SDK_INT >= 26) { + setOutputFile(recordOutputFile) + } else { + setOutputFile(recordOutputFile.path) + } + + prepare() + start() + callback() + } + } + + private fun pauseRecording() { + + } + + private fun stopRecording() { + + } + + private fun playRecordedAudio() { + + } + + private fun pauseRecordedAudio() { + + } + + private fun seekTo(ms: Float) { + + } + + private fun playAudio(descriptionAudio: Audio) { + + } + + private fun pauseAudio(descriptionAudio: Audio) { + + } + @@ -187,6 +255,14 @@ class ProfileMainFragment : Fragment() { val lazyColumState = rememberLazyListState() val profile by viewModel.profile.observeAsState() val followingState by viewModel.followingState.observeAsState() + val posts by viewModel.posts.collectAsState() + val loadingProfile by viewModel.loadingProfile.collectAsState() + val settingUpAudio by viewModel.settingUpAudio.collectAsState() + + val audioPlaybackState by viewModel.audioState.collectAsState() + + // use this to show or hide all audio related UI + val mediaControllerIsAvailable by viewModel.mediaControllerIsAvailable.collectAsState() val isAtTop by remember { derivedStateOf { lazyColumState.firstVisibleItemIndex == 0 && lazyColumState.firstVisibleItemScrollOffset == 0 @@ -254,8 +330,7 @@ class ProfileMainFragment : Fragment() { } - val posts by viewModel.posts.collectAsState() - val loadingProfile by viewModel.loadingProfile.collectAsState() + IsolaattiTheme { Scaffold( @@ -415,8 +490,77 @@ class ProfileMainFragment : Fragment() { onFollowChange = { follow -> viewModel.followUser() }, - profileAudioProgress = 0.4f, - showProfileAudioProgress = true, + audioPlayer = { + if(mediaControllerIsAvailable) { + when { + settingUpAudio -> { + AudioRecorder( + modifier = Modifier.fillMaxWidth(), + onDismiss = { + viewModel.settingUpAudio.value = false + }, + onPlay = { + playRecordedAudio() + }, + onPause = { + pauseRecordedAudio() + }, + onStopRecording = { + stopRecording() + }, + onPauseRecording = { + pauseRecording() + }, + onStartRecording = { + startRecording { + + } + }, + state = viewModel.audioRecorderState, + onSeek = { + + } + ) + } + it.descriptionAudio != null -> { + AudioPlayer( + modifier = Modifier.fillMaxWidth().height(120.dp), + positionMs = 2000L, + isPlaying = false, + durationMs = 30_000L, + isLoading = false, + onPlay = { + playAudio(it.descriptionAudio) + }, + onPause = { + pauseAudio(it.descriptionAudio) + }, + onSeek = { ms -> + seekTo(ms) + } + ) + } + it.isUserItself -> { + Card(Modifier.fillMaxWidth().height(120.dp).padding(16.dp)) { + Column(modifier = Modifier.fillMaxWidth().padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(stringResource(R.string.setup_your_profile_audio)) + TextButton( + onClick = { + viewModel.settingUpAudio.value = true + } + ) { + Text("Record audio") + } + } + } + } + } + } + + }, onEditProfileClick = { editProfile.launch(it) }, @@ -489,11 +633,26 @@ class ProfileMainFragment : Fragment() { } } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { mediaController = mediaControllerFuture.await() + viewModel.mediaControllerIsAvailable.value = true + + // check audios directory + directory = File(requireContext().filesDir, "recordings") + + withContext(Dispatchers.IO) { + if(!directory.isDirectory) { + if(directory.isFile) { + directory.delete() + } + directory.mkdir() + } + } + } viewLifecycleOwner.lifecycleScope.launch { diff --git a/app/src/main/java/com/isolaatti/profile/ui/components/ProfileHeader.kt b/app/src/main/java/com/isolaatti/profile/ui/components/ProfileHeader.kt index 927db55..bf3e0f6 100644 --- a/app/src/main/java/com/isolaatti/profile/ui/components/ProfileHeader.kt +++ b/app/src/main/java/com/isolaatti/profile/ui/components/ProfileHeader.kt @@ -69,8 +69,6 @@ fun ProfileHeader( followerCount: Int, followingCount: Int, onImageClick: () -> Unit, - showProfileAudioProgress: Boolean, - profileAudioProgress: Float, onFollowersClick: () -> Unit, onEditProfileClick: () -> Unit, onFollowChange: (Boolean) -> Unit, @@ -190,8 +188,6 @@ fun ProfileHeaderPreview() { isOwnUser = true, followingThisUser = true, onImageClick = {}, - profileAudioProgress = 0.6f, - showProfileAudioProgress = true, onFollowersClick = {}, onFollowChange = {}, onEditProfileClick = {}, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 73157f2..250e6d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -234,6 +234,7 @@ Change profile picture? Uploading photo You have no audio description + Setup your profile audio Spam Explicit content diff --git a/build.gradle b/build.gradle index 95ff731..7cf8fb7 100644 --- a/build.gradle +++ b/build.gradle @@ -14,8 +14,8 @@ buildscript { } } plugins { - id 'com.android.application' version '8.7.3' apply false - id 'com.android.library' version '8.7.3' apply false + id 'com.android.application' version '8.8.2' apply false + id 'com.android.library' version '8.8.2' apply false id 'org.jetbrains.kotlin.android' version '2.1.0' apply false id 'com.google.dagger.hilt.android' version '2.53.1' apply false id 'com.google.gms.google-services' version '4.4.2' apply false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b84dedc..46ddc9f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jul 27 21:16:21 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists