From 0aa7b1001f42f7b05c9f5550a5d7d175485b8eee Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sun, 3 Mar 2024 20:25:49 -0600 Subject: [PATCH] WIP reproduccion de audios en feeds y push notifications --- app/build.gradle | 2 +- .../main/java/com/isolaatti/MyApplication.kt | 27 ++ .../isolaatti/audio/common/data/AudioDto.kt | 3 +- .../isolaatti/audio/common/domain/Playable.kt | 2 + .../OnUserInteractedWithPostCallback.kt | 3 + .../java/com/isolaatti/home/FeedFragment.kt | 40 +++ .../presentation/NotificationsViewModel.kt | 7 - .../com/isolaatti/notifications/Module.kt | 25 ++ .../notifications/data/NotificationsApi.kt | 2 +- .../notifications/data/NotificationsDto.kt | 6 +- .../data/NotificationsRepositoryImpl.kt | 30 ++ .../notifications/domain/Notification.kt | 106 +++++++ .../domain/NotificationsRepository.kt | 8 + .../presentation/NotificationsAdapter.kt | 36 +++ .../presentation/NotificationsViewModel.kt | 11 + .../notifications/ui/NotificationsFragment.kt | 4 +- .../posting/posts/data/remote/FeedDto.kt | 8 +- .../posting/posts/domain/entity/Post.kt | 3 +- .../presentation/PostsRecyclerViewAdapter.kt | 275 ++++++++++++++---- .../profile/ui/ProfileMainFragment.kt | 49 +++- .../push_notifications/FcmService.kt | 22 +- .../PushNotificationsApi.kt | 2 +- app/src/main/res/layout/notification_item.xml | 65 +++++ app/src/main/res/layout/post_layout.xml | 5 + .../main/res/navigation/home_navigation.xml | 2 +- 25 files changed, 664 insertions(+), 79 deletions(-) delete mode 100644 app/src/main/java/com/isolaatti/home/notifications/presentation/NotificationsViewModel.kt create mode 100644 app/src/main/java/com/isolaatti/notifications/Module.kt rename app/src/main/java/com/isolaatti/{home => }/notifications/data/NotificationsApi.kt (82%) rename app/src/main/java/com/isolaatti/{home => }/notifications/data/NotificationsDto.kt (71%) create mode 100644 app/src/main/java/com/isolaatti/notifications/data/NotificationsRepositoryImpl.kt create mode 100644 app/src/main/java/com/isolaatti/notifications/domain/Notification.kt create mode 100644 app/src/main/java/com/isolaatti/notifications/domain/NotificationsRepository.kt create mode 100644 app/src/main/java/com/isolaatti/notifications/presentation/NotificationsAdapter.kt create mode 100644 app/src/main/java/com/isolaatti/notifications/presentation/NotificationsViewModel.kt rename app/src/main/java/com/isolaatti/{home => }/notifications/ui/NotificationsFragment.kt (87%) create mode 100644 app/src/main/res/layout/notification_item.xml diff --git a/app/build.gradle b/app/build.gradle index 075282a..9ca0603 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ android { minSdk 24 targetSdk 34 versionCode 1 - versionName "1.0" + versionName "0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/isolaatti/MyApplication.kt b/app/src/main/java/com/isolaatti/MyApplication.kt index 6ec689a..1f65939 100644 --- a/app/src/main/java/com/isolaatti/MyApplication.kt +++ b/app/src/main/java/com/isolaatti/MyApplication.kt @@ -3,11 +3,21 @@ package com.isolaatti import android.app.Application import android.content.Context import android.net.ConnectivityManager +import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore +import com.google.firebase.Firebase +import com.google.firebase.messaging.FirebaseMessaging import com.isolaatti.connectivity.ConnectivityCallbackImpl +import com.isolaatti.push_notifications.FcmService +import com.isolaatti.push_notifications.PushNotificationsApi import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import retrofit2.awaitResponse +import javax.inject.Inject val Context.dataStore: DataStore by preferencesDataStore(name = "settings") @@ -16,8 +26,12 @@ class MyApplication : Application() { companion object { lateinit var myApp: MyApplication + const val LOG_TAG = "MyApplication" } + @Inject + lateinit var pushNotificationsApi: PushNotificationsApi + private val activityLifecycleCallbacks = ActivityLifecycleCallbacks() lateinit var connectivityCallbackImpl: ConnectivityCallbackImpl @@ -27,6 +41,19 @@ class MyApplication : Application() { registerActivityLifecycleCallbacks(activityLifecycleCallbacks) connectivityCallbackImpl = ConnectivityCallbackImpl() getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(connectivityCallbackImpl) + + FirebaseMessaging.getInstance().token.addOnCompleteListener { + if(!it.isSuccessful) { + Log.w(LOG_TAG, "Failed fetching fcm token") + return@addOnCompleteListener + } + + CoroutineScope(Dispatchers.IO).launch { + val response = pushNotificationsApi.registerDevice(it.result).awaitResponse() + Log.d(FcmService.LOG_TAG, "Device registered. FCM token: $it.result") + Log.d(FcmService.LOG_TAG, "Response: isSuccessful: ${response.isSuccessful}") + } + } } override fun onTerminate() { diff --git a/app/src/main/java/com/isolaatti/audio/common/data/AudioDto.kt b/app/src/main/java/com/isolaatti/audio/common/data/AudioDto.kt index 953998a..ff458ff 100644 --- a/app/src/main/java/com/isolaatti/audio/common/data/AudioDto.kt +++ b/app/src/main/java/com/isolaatti/audio/common/data/AudioDto.kt @@ -1,5 +1,6 @@ package com.isolaatti.audio.common.data +import java.io.Serializable import java.time.ZonedDateTime data class AudiosDto(val data: List) @@ -10,4 +11,4 @@ data class AudioDto( val userId: Int, val firestoreObjectPath: String, val userName: String -) \ No newline at end of file +): Serializable \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/audio/common/domain/Playable.kt b/app/src/main/java/com/isolaatti/audio/common/domain/Playable.kt index 47ac73a..a610d3b 100644 --- a/app/src/main/java/com/isolaatti/audio/common/domain/Playable.kt +++ b/app/src/main/java/com/isolaatti/audio/common/domain/Playable.kt @@ -6,6 +6,8 @@ abstract class Playable { var isPlaying: Boolean = false abstract val uri: Uri var isLoading: Boolean = false + var progress: Int = 0 + var duration: Int = 0 /** * Image url, null indicating no image should be shown diff --git a/app/src/main/java/com/isolaatti/common/OnUserInteractedWithPostCallback.kt b/app/src/main/java/com/isolaatti/common/OnUserInteractedWithPostCallback.kt index bda7fc4..5c879e1 100644 --- a/app/src/main/java/com/isolaatti/common/OnUserInteractedWithPostCallback.kt +++ b/app/src/main/java/com/isolaatti/common/OnUserInteractedWithPostCallback.kt @@ -1,8 +1,11 @@ package com.isolaatti.common +import com.isolaatti.audio.common.domain.Audio + interface OnUserInteractedWithPostCallback : OnUserInteractedCallback { fun onLiked(postId: Long) fun onUnLiked(postId: Long) fun onComment(postId: Long) fun onOpenPost(postId: Long) + fun onPlay(audio: Audio) } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/FeedFragment.kt b/app/src/main/java/com/isolaatti/home/FeedFragment.kt index d0b2a1b..a281b72 100644 --- a/app/src/main/java/com/isolaatti/home/FeedFragment.kt +++ b/app/src/main/java/com/isolaatti/home/FeedFragment.kt @@ -22,6 +22,9 @@ import com.google.android.material.card.MaterialCardView import com.isolaatti.BuildConfig import com.isolaatti.R import com.isolaatti.about.AboutActivity +import com.isolaatti.audio.common.domain.Audio +import com.isolaatti.audio.common.domain.Playable +import com.isolaatti.audio.player.AudioPlayerConnector import com.isolaatti.common.CoilImageLoader.imageLoader import com.isolaatti.common.Dialogs import com.isolaatti.common.ErrorMessageViewModel @@ -70,6 +73,8 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { private lateinit var viewBinding: FragmentFeedBinding private lateinit var adapter: PostsRecyclerViewAdapter + private lateinit var audioPlayerConnector: AudioPlayerConnector + // region launchers private val createDiscussion = registerForActivityResult(CreatePostContract()) { @@ -127,6 +132,34 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { // endregion + private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener { + override fun onPlaying(isPlaying: Boolean, audio: Playable) { + if(audio is Audio) + adapter.setIsPlaying(isPlaying, audio) + } + + override fun isLoading(isLoading: Boolean, audio: Playable) { + if(audio is Audio) + adapter.setIsLoading(isLoading, audio) + } + + override fun progressChanged(second: Int, audio: Playable) { + if(audio is Audio) + adapter.setProgress(second, audio) + } + + override fun durationChanged(duration: Int, audio: Playable) { + if(audio is Audio) + adapter.setDuration(duration, audio) + } + + override fun onEnded(audio: Playable) { + if(audio is Audio) + adapter.setEnded(audio) + } + + } + // region events override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -149,6 +182,9 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { } } + audioPlayerConnector = AudioPlayerConnector(requireContext()) + audioPlayerConnector.addListener(audioPlayerConnectorListener) + viewLifecycleOwner.lifecycle.addObserver(audioPlayerConnector) val markwon = Markwon.builder(requireContext()) .usePlugin(object: AbstractMarkwonPlugin() { @@ -258,6 +294,10 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { PostViewerActivity.startActivity(requireContext(), postId) } + override fun onPlay(audio: Audio) { + audioPlayerConnector.playPauseAudio(audio) + } + override fun onProfileClick(userId: Int) { ProfileActivity.startActivity(requireContext(), userId) } diff --git a/app/src/main/java/com/isolaatti/home/notifications/presentation/NotificationsViewModel.kt b/app/src/main/java/com/isolaatti/home/notifications/presentation/NotificationsViewModel.kt deleted file mode 100644 index 8e52cab..0000000 --- a/app/src/main/java/com/isolaatti/home/notifications/presentation/NotificationsViewModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.isolaatti.home.notifications.presentation - -import androidx.lifecycle.ViewModel - -class NotificationsViewModel : ViewModel() { - // TODO: Implement the ViewModel -} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/notifications/Module.kt b/app/src/main/java/com/isolaatti/notifications/Module.kt new file mode 100644 index 0000000..7c44d53 --- /dev/null +++ b/app/src/main/java/com/isolaatti/notifications/Module.kt @@ -0,0 +1,25 @@ +package com.isolaatti.notifications + +import com.isolaatti.connectivity.RetrofitClient +import com.isolaatti.notifications.data.NotificationsApi +import com.isolaatti.notifications.data.NotificationsRepositoryImpl +import com.isolaatti.notifications.domain.NotificationsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class Module { + + @Provides + fun provideNotificationsApi(retrofitClient: RetrofitClient): NotificationsApi { + return retrofitClient.client.create(NotificationsApi::class.java) + } + + @Provides + fun provideNotificationsRepository(notificationsApi: NotificationsApi): NotificationsRepository { + return NotificationsRepositoryImpl(notificationsApi) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/notifications/data/NotificationsApi.kt b/app/src/main/java/com/isolaatti/notifications/data/NotificationsApi.kt similarity index 82% rename from app/src/main/java/com/isolaatti/home/notifications/data/NotificationsApi.kt rename to app/src/main/java/com/isolaatti/notifications/data/NotificationsApi.kt index 7c9c1b0..5d2d850 100644 --- a/app/src/main/java/com/isolaatti/home/notifications/data/NotificationsApi.kt +++ b/app/src/main/java/com/isolaatti/notifications/data/NotificationsApi.kt @@ -1,4 +1,4 @@ -package com.isolaatti.home.notifications.data +package com.isolaatti.notifications.data import retrofit2.Call import retrofit2.http.GET diff --git a/app/src/main/java/com/isolaatti/home/notifications/data/NotificationsDto.kt b/app/src/main/java/com/isolaatti/notifications/data/NotificationsDto.kt similarity index 71% rename from app/src/main/java/com/isolaatti/home/notifications/data/NotificationsDto.kt rename to app/src/main/java/com/isolaatti/notifications/data/NotificationsDto.kt index 62aa82e..5c762f9 100644 --- a/app/src/main/java/com/isolaatti/home/notifications/data/NotificationsDto.kt +++ b/app/src/main/java/com/isolaatti/notifications/data/NotificationsDto.kt @@ -1,5 +1,6 @@ -package com.isolaatti.home.notifications.data +package com.isolaatti.notifications.data +import com.google.gson.internal.LinkedTreeMap import java.time.ZonedDateTime data class NotificationsDto( @@ -18,5 +19,6 @@ data class NotificationPayload( val type: String, val authorId: Int, val authorName: String?, - val intentData: String? + val intentData: String?, + val data: Map ) diff --git a/app/src/main/java/com/isolaatti/notifications/data/NotificationsRepositoryImpl.kt b/app/src/main/java/com/isolaatti/notifications/data/NotificationsRepositoryImpl.kt new file mode 100644 index 0000000..f2e16ec --- /dev/null +++ b/app/src/main/java/com/isolaatti/notifications/data/NotificationsRepositoryImpl.kt @@ -0,0 +1,30 @@ +package com.isolaatti.notifications.data + +import android.util.Log +import com.isolaatti.notifications.domain.Notification +import com.isolaatti.notifications.domain.NotificationsRepository +import com.isolaatti.utils.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import retrofit2.awaitResponse + +class NotificationsRepositoryImpl(private val notificationsApi: NotificationsApi) : NotificationsRepository { + companion object { + const val LOG_TAG = "NotificationsRepositoryImpl" + } + override fun getNotifications(after: Long?): Flow>> = flow { + try { + val response = notificationsApi.getNotifications(after).awaitResponse() + + if(response.isSuccessful) { + + } else { + Log.e(LOG_TAG, "getNotifications(): Request is not successful, response code is ${response.code()}") + emit(Resource.Error(Resource.Error.mapErrorCode(response.code()))) + } + } catch(e: Exception) { + Log.e(LOG_TAG, e.message.toString()) + emit(Resource.Error(Resource.Error.ErrorType.OtherError)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/notifications/domain/Notification.kt b/app/src/main/java/com/isolaatti/notifications/domain/Notification.kt new file mode 100644 index 0000000..d769aba --- /dev/null +++ b/app/src/main/java/com/isolaatti/notifications/domain/Notification.kt @@ -0,0 +1,106 @@ +package com.isolaatti.notifications.domain + +import com.isolaatti.databinding.NotificationItemBinding +import com.isolaatti.notifications.data.NotificationDto +import com.isolaatti.notifications.data.NotificationPayload +import java.time.ZonedDateTime + + +class GenericNotification(id: Long, date: ZonedDateTime, userId: Int, read: Boolean) : Notification(id, date, userId, read) { + + var title: String? = null + var message: String? = null + + override fun ingestPayload(notificationPayload: NotificationPayload) { + + } + + override fun bind(notificationBinding: NotificationItemBinding) { + + } + + companion object { + const val TYPE = "generic" + } +} + +class LikeNotification(id: Long, date: ZonedDateTime, userId: Int, read: Boolean) : Notification(id, date, userId, read) { + companion object { + const val TYPE = "like" + } + + override fun ingestPayload(notificationPayload: NotificationPayload) { + TODO("Not yet implemented") + } + + override fun bind(notificationBinding: NotificationItemBinding) { + TODO("Not yet implemented") + } +} + +class FollowNotification(id: Long, date: ZonedDateTime, userId: Int, read: Boolean) : Notification(id, date, userId, read) { + + companion object { + const val TYPE = "follow" + } + + override fun ingestPayload(notificationPayload: NotificationPayload) { + TODO("Not yet implemented") + } + + override fun bind(notificationBinding: NotificationItemBinding) { + TODO("Not yet implemented") + } +} + + +abstract class Notification( + val id: Long, + val date: ZonedDateTime, + val userId: Int, + var read: Boolean +) { + + abstract fun ingestPayload(notificationPayload: NotificationPayload) + + abstract fun bind(notificationBinding: NotificationItemBinding) + + companion object { + fun fromDto(notificationDto: NotificationDto): Notification? { + return when(notificationDto.payload.type) { + GenericNotification.TYPE -> { + + GenericNotification( + notificationDto.id, + notificationDto.date, + notificationDto.userId, + notificationDto.read + ).apply { + ingestPayload(notificationDto.payload) + } + } + LikeNotification.TYPE -> { + LikeNotification( + notificationDto.id, + notificationDto.date, + notificationDto.userId, + notificationDto.read + ).apply { + ingestPayload(notificationDto.payload) + } + } + FollowNotification.TYPE -> { + FollowNotification( + notificationDto.id, + notificationDto.date, + notificationDto.userId, + notificationDto.read + ).apply { + ingestPayload(notificationDto.payload) + } + } + else -> null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/notifications/domain/NotificationsRepository.kt b/app/src/main/java/com/isolaatti/notifications/domain/NotificationsRepository.kt new file mode 100644 index 0000000..5a4d9c4 --- /dev/null +++ b/app/src/main/java/com/isolaatti/notifications/domain/NotificationsRepository.kt @@ -0,0 +1,8 @@ +package com.isolaatti.notifications.domain + +import com.isolaatti.utils.Resource +import kotlinx.coroutines.flow.Flow + +interface NotificationsRepository { + fun getNotifications(after: Long?): Flow>> +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/notifications/presentation/NotificationsAdapter.kt b/app/src/main/java/com/isolaatti/notifications/presentation/NotificationsAdapter.kt new file mode 100644 index 0000000..38e4a6c --- /dev/null +++ b/app/src/main/java/com/isolaatti/notifications/presentation/NotificationsAdapter.kt @@ -0,0 +1,36 @@ +package com.isolaatti.notifications.presentation + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.isolaatti.databinding.NotificationItemBinding +import com.isolaatti.notifications.domain.Notification + +class NotificationsAdapter : ListAdapter( + diffCallback +) { + inner class NotificationViewHolder(val notificationItemBinding: NotificationItemBinding) : ViewHolder(notificationItemBinding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder { + return NotificationViewHolder(NotificationItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + + override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) { + getItem(position).bind(holder.notificationItemBinding) + } + + companion object { + val diffCallback = object: DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean { + TODO("Not yet implemented") + } + + override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean { + TODO("Not yet implemented") + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/notifications/presentation/NotificationsViewModel.kt b/app/src/main/java/com/isolaatti/notifications/presentation/NotificationsViewModel.kt new file mode 100644 index 0000000..816c125 --- /dev/null +++ b/app/src/main/java/com/isolaatti/notifications/presentation/NotificationsViewModel.kt @@ -0,0 +1,11 @@ +package com.isolaatti.notifications.presentation + +import androidx.lifecycle.ViewModel +import com.isolaatti.notifications.domain.NotificationsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class NotificationsViewModel @Inject constructor(private val notificationsRepository: NotificationsRepository) : ViewModel() { + +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/notifications/ui/NotificationsFragment.kt b/app/src/main/java/com/isolaatti/notifications/ui/NotificationsFragment.kt similarity index 87% rename from app/src/main/java/com/isolaatti/home/notifications/ui/NotificationsFragment.kt rename to app/src/main/java/com/isolaatti/notifications/ui/NotificationsFragment.kt index 8e7af2e..6f77631 100644 --- a/app/src/main/java/com/isolaatti/home/notifications/ui/NotificationsFragment.kt +++ b/app/src/main/java/com/isolaatti/notifications/ui/NotificationsFragment.kt @@ -1,4 +1,4 @@ -package com.isolaatti.home.notifications.ui +package com.isolaatti.notifications.ui import androidx.lifecycle.ViewModelProvider import android.os.Bundle @@ -7,7 +7,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.isolaatti.R -import com.isolaatti.home.notifications.presentation.NotificationsViewModel +import com.isolaatti.notifications.presentation.NotificationsViewModel class NotificationsFragment : Fragment() { diff --git a/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedDto.kt b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedDto.kt index 7a33375..629bcc6 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedDto.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedDto.kt @@ -2,6 +2,7 @@ package com.isolaatti.posting.posts.data.remote import android.os.Parcel import android.os.Parcelable +import com.isolaatti.audio.common.data.AudioDto import java.io.Serializable data class FeedDto( @@ -23,7 +24,8 @@ data class FeedDto( var numberOfComments: Int, val userName: String, val squadName: String?, - var liked: Boolean + var liked: Boolean, + var audio: AudioDto? ): Parcelable { constructor(parcel: Parcel) : this( @@ -32,7 +34,8 @@ data class FeedDto( parcel.readInt(), parcel.readString()!!, parcel.readString(), - parcel.readByte() != 0.toByte() + parcel.readByte() != 0.toByte(), + parcel.readParcelable(AudioDto::class.java.classLoader) ) data class Post( @@ -93,6 +96,7 @@ data class FeedDto( parcel.writeString(userName) parcel.writeString(squadName) parcel.writeByte(if (liked) 1 else 0) + parcel.writeSerializable(audio) } override fun describeContents(): Int { 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 b9dce65..df720b5 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 @@ -54,7 +54,8 @@ data class Post( numberOfLikes = it.numberOfLikes, userName = it.userName, squadName = it.squadName, - liked = it.liked + liked = it.liked, + audio = it.audio?.let { audioDto -> Audio.fromDto(audioDto) } ) }.toMutableList() } diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt index cead8b3..445dc82 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt @@ -7,107 +7,149 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import coil.load import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.isolaatti.R +import com.isolaatti.audio.common.domain.Audio import com.isolaatti.common.CoilImageLoader.imageLoader import com.isolaatti.common.OnUserInteractedWithPostCallback +import com.isolaatti.databinding.PostLayoutBinding import com.isolaatti.posting.posts.domain.entity.Post import com.isolaatti.utils.UrlGen.userProfileImage import io.noties.markwon.Markwon -class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callback: OnUserInteractedWithPostCallback) : RecyclerView.Adapter(){ +class PostsRecyclerViewAdapter ( + private val markwon: Markwon, + private val callback: OnUserInteractedWithPostCallback +) : RecyclerView.Adapter(){ private var postList: List? = null - inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) { - fun bindView(postDto: Post, payloads: List) { - - Log.d("payloads", payloads.count().toString()) - val likeButton: MaterialButton = itemView.findViewById(R.id.like_button) - val commentsButton: MaterialButton = itemView.findViewById(R.id.comment_button) + inner class FeedViewHolder(val itemBinding: PostLayoutBinding) : ViewHolder(itemBinding.root) { + fun bindView(post: Post, payloads: List) { if(payloads.isNotEmpty()) { for(payload in payloads) { - when(payload) { - is LikeCountUpdatePayload -> { + when { + payload is LikeCountUpdatePayload -> { + itemBinding.likeButton.isEnabled = true - - likeButton.isEnabled = true - - if(postDto.liked) { - likeButton.setIconTintResource(R.color.purple_lighter) - likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter)) + if(post.liked) { + itemBinding.likeButton.setIconTintResource(R.color.purple_lighter) + itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter)) } else { - likeButton.setIconTintResource(R.color.on_surface) - likeButton.setTextColor(itemView.context.getColor(R.color.on_surface)) + itemBinding.likeButton.setIconTintResource(R.color.on_surface) + itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.on_surface)) } - likeButton.text = postDto.numberOfLikes.toString() - - + itemBinding.likeButton.text = post.numberOfLikes.toString() } - - is CommentsCountUpdatePayload -> { - - commentsButton.text = postDto.numberOfComments.toString() - + payload is CommentsCountUpdatePayload -> { + itemBinding.commentButton.text = post.numberOfComments.toString() + } + payload is AudioEventPayload && payload == AudioEventPayload.IsPLaying -> { + val audio = post.audio + if(audio != null){ + itemBinding.audio.playButton.icon = + AppCompatResources.getDrawable( + itemView.context, + if(audio.isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24 + ) + } + } + payload is AudioEventPayload && payload == AudioEventPayload.ProgressChanged -> { + val audio = post.audio + if(audio != null){ + itemBinding.audio.audioProgress.progress = audio.progress + } + } + payload is AudioEventPayload && payload == AudioEventPayload.IsLoading -> { + val audio = post.audio + if(audio != null){ + itemBinding.audio.audioProgress.isIndeterminate = audio.isLoading + } + } + payload is AudioEventPayload && payload == AudioEventPayload.DurationChanged -> { + val audio = post.audio + if(audio != null){ + itemBinding.audio.audioProgress.max = audio.duration + } + } + payload is AudioEventPayload && payload == AudioEventPayload.Ended -> { + val audio = post.audio + if(audio != null){ + itemBinding.audio.audioProgress.progress = 0 + itemBinding.audio.playButton.icon = AppCompatResources.getDrawable(itemView.context, R.drawable.baseline_play_circle_24) + } } } } } else { val username: TextView = itemView.findViewById(R.id.text_view_username) - username.text = postDto.userName + username.text = post.userName username.setOnClickListener { - callback.onProfileClick(postDto.userId) + callback.onProfileClick(post.userId) } val profileImageView: ImageView = itemView.findViewById(R.id.avatar_picture) - profileImageView.load(userProfileImage(postDto.userId), imageLoader) + profileImageView.load(userProfileImage(post.userId), imageLoader) val dateTextView: TextView = itemView.findViewById(R.id.text_view_date) - dateTextView.text = postDto.date + dateTextView.text = post.date val content: TextView = itemView.findViewById(R.id.post_content) - markwon.setMarkdown(content, postDto.textContent) + markwon.setMarkdown(content, post.textContent) - likeButton.isEnabled = true + itemBinding.likeButton.isEnabled = true - if(postDto.liked) { - likeButton.setIconTintResource(R.color.purple_lighter) - likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter)) + if(post.liked) { + itemBinding.likeButton.setIconTintResource(R.color.purple_lighter) + itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter)) } else { - likeButton.setIconTintResource(R.color.on_surface) - likeButton.setTextColor(itemView.context.getColor(R.color.on_surface)) + itemBinding.likeButton.setIconTintResource(R.color.on_surface) + itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.on_surface)) } - likeButton.text = postDto.numberOfLikes.toString() + itemBinding.likeButton.text = post.numberOfLikes.toString() - commentsButton.text = postDto.numberOfComments.toString() + itemBinding.commentButton.text = post.numberOfComments.toString() val moreButton: MaterialButton = itemView.findViewById(R.id.more_button) moreButton.setOnClickListener { - callback.onOptions(postDto) + callback.onOptions(post) } - likeButton.setOnClickListener { - likeButton.isEnabled = false - if(postDto.liked){ - callback.onUnLiked(postDto.id) + itemBinding.likeButton.setOnClickListener { + itemBinding.likeButton.isEnabled = false + if(post.liked){ + callback.onUnLiked(post.id) } else { - callback.onLiked(postDto.id) + callback.onLiked(post.id) } } - commentsButton.setOnClickListener { - callback.onComment(postDto.id) + itemBinding.commentButton.setOnClickListener { + callback.onComment(post.id) } itemView.findViewById(R.id.card).setOnClickListener { - callback.onOpenPost(postDto.id) + callback.onOpenPost(post.id) + } + if(post.audio != null){ + itemBinding.audio.apply { + root.visibility = View.VISIBLE + textViewDescription.text = post.audio.name + } + itemBinding.audio.playButton.setOnClickListener { + callback.onPlay(post.audio) + } + } else { + itemBinding.audio.root.visibility = View.GONE + itemBinding.audio.playButton.setOnClickListener(null) } - } } } @@ -116,11 +158,138 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba data class LikeCountUpdatePayload(val likeCount: Int) data class CommentsCountUpdatePayload(val commentsCount: Int) + private var currentAudio: Audio? = null + private var currentAudioPosition: Int = -1 + enum class AudioEventPayload { + ProgressChanged, IsLoading, IsPLaying, DurationChanged, Ended + } + + fun setIsPlaying(isPlaying: Boolean, audio: Audio) { + if(audio == currentAudio) { + currentAudio?.isPlaying = isPlaying + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.IsPLaying) + } + return + } + currentAudio?.isPlaying = false + + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.IsPLaying) + } else { + if(postList != null) { + for((index, post) in postList!!.withIndex()){ + post.audio?.isPlaying = false + post.audio?.progress = 0 + post.audio?.isLoading = false + if(post.audio != null) { + notifyItemChanged(index) + } + } + + } + } + + currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1 + Log.d(LOG_TAG, "setIsPlaying currentAudioPosition: $currentAudioPosition") + + if(currentAudioPosition > -1) { + currentAudio = postList?.get(currentAudioPosition)?.audio?.also { it.isPlaying = isPlaying } + notifyItemChanged(currentAudioPosition, AudioEventPayload.IsPLaying) + } + } + + fun setIsLoading(isLoading: Boolean, audio: Audio) { + if(audio == currentAudio) { + currentAudio?.isLoading = isLoading + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.IsLoading) + } + return + } + currentAudio?.isPlaying = false + currentAudio?.isLoading = false + + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.IsLoading) + } + + currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1 + + Log.d(LOG_TAG, "setIsLoading currentAudioPosition: $currentAudioPosition") + + if(currentAudioPosition > -1) { + postList?.get(currentAudioPosition)?.audio?.isLoading = isLoading + notifyItemChanged(currentAudioPosition, AudioEventPayload.IsLoading) + } + } + + fun setProgress(progress: Int, audio: Audio){ + if(audio == currentAudio) { + audio.progress = progress + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged) + } + return + } + currentAudio?.isPlaying = false + currentAudio?.progress = 0 + + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged) + } + + currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1 + + + if(currentAudioPosition > -1) { + postList?.get(currentAudioPosition)?.audio?.progress = progress + notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged) + } + } + + fun setDuration(duration: Int, audio: Audio) { + if(audio == currentAudio) { + audio.duration = duration + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged) + } + return + } + currentAudio?.isPlaying = false + + currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1 + + if(currentAudioPosition > -1) { + postList?.get(currentAudioPosition)?.audio?.duration = duration + notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged) + } + } + + fun setEnded(audio: Audio) { + if(audio == currentAudio) { + audio.isPlaying = false + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged) + } + return + } + currentAudio?.isPlaying = false + + if(currentAudioPosition > -1) { + notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged) + } + + currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1 + + if(currentAudioPosition > -1) { + postList?.get(currentAudioPosition)?.audio?.isPlaying = false + notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged) + } + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.post_layout, parent, false) - - return FeedViewHolder(view) + return FeedViewHolder(PostLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)) } var previousSize = 0 @@ -197,4 +366,8 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba } } + + companion object { + const val LOG_TAG = "PostsRecyclerViewAdapter" + } } \ No newline at end of file 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 52e9355..a6a614d 100644 --- a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt +++ b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt @@ -106,27 +106,57 @@ class ProfileMainFragment : Fragment() { } } + private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener { 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) - + Log.d(LOG_TAG, "onPlaying() isPlaying: $isPlaying: audio $audio") + if(audio == audioDescriptionAudio) { + viewBinding.playButton.icon = + AppCompatResources.getDrawable( + requireContext(), + if(isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24 + ) + } + if(audio is Audio) + postsAdapter.setIsPlaying(isPlaying, audio) } override fun isLoading(isLoading: Boolean, audio: Playable) { - viewBinding.playButton.isEnabled = !isLoading - viewBinding.audioProgress.isIndeterminate = isLoading + if(audio == audioDescriptionAudio) { + viewBinding.playButton.isEnabled = !isLoading + viewBinding.audioProgress.isIndeterminate = isLoading + } + + if(audio is Audio) + postsAdapter.setIsLoading(isLoading, audio) + } override fun progressChanged(second: Int, audio: Playable) { - viewBinding.audioProgress.setProgress(second, true) + if(audio == audioDescriptionAudio) { + viewBinding.audioProgress.setProgress(second, true) + } + + if(audio is Audio) + postsAdapter.setProgress(second, audio) } override fun durationChanged(duration: Int, audio: Playable) { - viewBinding.audioProgress.max = duration + if(audio == audioDescriptionAudio) { + viewBinding.audioProgress.max = duration + } + + if(audio is Audio) + postsAdapter.setDuration(duration, audio) } override fun onEnded(audio: Playable) { - viewBinding.audioProgress.progress = 0 + if(audio == audioDescriptionAudio) { + viewBinding.audioProgress.progress = 0 + } + + if(audio is Audio) + postsAdapter.setEnded(audio) } } @@ -431,6 +461,10 @@ class ProfileMainFragment : Fragment() { //ProfileActivity.startActivity(requireContext(), userId) } + override fun onPlay(audio: Audio) { + audioPlayerConnector.playPauseAudio(audio) + } + override fun onLoadMore() { viewModel.getFeed(false) } @@ -474,5 +508,6 @@ class ProfileMainFragment : Fragment() { companion object { const val CALLER_ID = 30 + const val LOG_TAG = "ProfileMainFragment" } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/push_notifications/FcmService.kt b/app/src/main/java/com/isolaatti/push_notifications/FcmService.kt index 5702a40..c3d3d81 100644 --- a/app/src/main/java/com/isolaatti/push_notifications/FcmService.kt +++ b/app/src/main/java/com/isolaatti/push_notifications/FcmService.kt @@ -4,8 +4,13 @@ import android.Manifest import android.util.Log import androidx.core.content.ContextCompat import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage import dagger.hilt.EntryPoint import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import retrofit2.awaitResponse import javax.inject.Inject @AndroidEntryPoint @@ -19,7 +24,20 @@ class FcmService : FirebaseMessagingService() { override fun onNewToken(token: String) { - pushNotificationsApi.registerDevice(token) - Log.d(LOG_TAG, token) + CoroutineScope(Dispatchers.IO).launch { + val response = pushNotificationsApi.registerDevice(token).awaitResponse() + Log.d(LOG_TAG, "Device registered. FCM token: $token") + Log.d(LOG_TAG, "Response: isSuccessful: ${response.isSuccessful}") + } + + } + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + + Log.d(LOG_TAG, "Message received") + message.data.forEach { t, u -> + Log.d(LOG_TAG, "$t $u") + } } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/push_notifications/PushNotificationsApi.kt b/app/src/main/java/com/isolaatti/push_notifications/PushNotificationsApi.kt index 26e2430..bfd97a7 100644 --- a/app/src/main/java/com/isolaatti/push_notifications/PushNotificationsApi.kt +++ b/app/src/main/java/com/isolaatti/push_notifications/PushNotificationsApi.kt @@ -9,6 +9,6 @@ interface PushNotificationsApi { @PUT("/api/push_notifications/register_device") @Multipart - fun registerDevice(@Part("token") token: String): Call + fun registerDevice(@Part("token") token: String): Call } \ No newline at end of file diff --git a/app/src/main/res/layout/notification_item.xml b/app/src/main/res/layout/notification_item.xml new file mode 100644 index 0000000..3bce909 --- /dev/null +++ b/app/src/main/res/layout/notification_item.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/post_layout.xml b/app/src/main/res/layout/post_layout.xml index bea1734..2e0dd02 100644 --- a/app/src/main/res/layout/post_layout.xml +++ b/app/src/main/res/layout/post_layout.xml @@ -56,6 +56,11 @@ android:gravity="end"/> +