WIP reproduccion de audios en feeds y push notifications

This commit is contained in:
erik-everardo 2024-03-03 20:25:49 -06:00
parent c87e12caab
commit 0aa7b1001f
25 changed files with 664 additions and 79 deletions

View File

@ -23,7 +23,7 @@ android {
minSdk 24 minSdk 24
targetSdk 34 targetSdk 34
versionCode 1 versionCode 1
versionName "1.0" versionName "0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@ -3,11 +3,21 @@ package com.isolaatti
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.util.Log
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.google.firebase.Firebase
import com.google.firebase.messaging.FirebaseMessaging
import com.isolaatti.connectivity.ConnectivityCallbackImpl import com.isolaatti.connectivity.ConnectivityCallbackImpl
import com.isolaatti.push_notifications.FcmService
import com.isolaatti.push_notifications.PushNotificationsApi
import dagger.hilt.android.HiltAndroidApp 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<Preferences> by preferencesDataStore(name = "settings") val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
@ -16,8 +26,12 @@ class MyApplication : Application() {
companion object { companion object {
lateinit var myApp: MyApplication lateinit var myApp: MyApplication
const val LOG_TAG = "MyApplication"
} }
@Inject
lateinit var pushNotificationsApi: PushNotificationsApi
private val activityLifecycleCallbacks = ActivityLifecycleCallbacks() private val activityLifecycleCallbacks = ActivityLifecycleCallbacks()
lateinit var connectivityCallbackImpl: ConnectivityCallbackImpl lateinit var connectivityCallbackImpl: ConnectivityCallbackImpl
@ -27,6 +41,19 @@ class MyApplication : Application() {
registerActivityLifecycleCallbacks(activityLifecycleCallbacks) registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
connectivityCallbackImpl = ConnectivityCallbackImpl() connectivityCallbackImpl = ConnectivityCallbackImpl()
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(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() { override fun onTerminate() {

View File

@ -1,5 +1,6 @@
package com.isolaatti.audio.common.data package com.isolaatti.audio.common.data
import java.io.Serializable
import java.time.ZonedDateTime import java.time.ZonedDateTime
data class AudiosDto(val data: List<AudioDto>) data class AudiosDto(val data: List<AudioDto>)
@ -10,4 +11,4 @@ data class AudioDto(
val userId: Int, val userId: Int,
val firestoreObjectPath: String, val firestoreObjectPath: String,
val userName: String val userName: String
) ): Serializable

View File

@ -6,6 +6,8 @@ abstract class Playable {
var isPlaying: Boolean = false var isPlaying: Boolean = false
abstract val uri: Uri abstract val uri: Uri
var isLoading: Boolean = false var isLoading: Boolean = false
var progress: Int = 0
var duration: Int = 0
/** /**
* Image url, null indicating no image should be shown * Image url, null indicating no image should be shown

View File

@ -1,8 +1,11 @@
package com.isolaatti.common package com.isolaatti.common
import com.isolaatti.audio.common.domain.Audio
interface OnUserInteractedWithPostCallback : OnUserInteractedCallback { interface OnUserInteractedWithPostCallback : OnUserInteractedCallback {
fun onLiked(postId: Long) fun onLiked(postId: Long)
fun onUnLiked(postId: Long) fun onUnLiked(postId: Long)
fun onComment(postId: Long) fun onComment(postId: Long)
fun onOpenPost(postId: Long) fun onOpenPost(postId: Long)
fun onPlay(audio: Audio)
} }

View File

@ -22,6 +22,9 @@ import com.google.android.material.card.MaterialCardView
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.about.AboutActivity 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.CoilImageLoader.imageLoader
import com.isolaatti.common.Dialogs import com.isolaatti.common.Dialogs
import com.isolaatti.common.ErrorMessageViewModel import com.isolaatti.common.ErrorMessageViewModel
@ -70,6 +73,8 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
private lateinit var viewBinding: FragmentFeedBinding private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: PostsRecyclerViewAdapter private lateinit var adapter: PostsRecyclerViewAdapter
private lateinit var audioPlayerConnector: AudioPlayerConnector
// region launchers // region launchers
private val createDiscussion = registerForActivityResult(CreatePostContract()) { private val createDiscussion = registerForActivityResult(CreatePostContract()) {
@ -127,6 +132,34 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
// endregion // 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 // region events
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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()) val markwon = Markwon.builder(requireContext())
.usePlugin(object: AbstractMarkwonPlugin() { .usePlugin(object: AbstractMarkwonPlugin() {
@ -258,6 +294,10 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
PostViewerActivity.startActivity(requireContext(), postId) PostViewerActivity.startActivity(requireContext(), postId)
} }
override fun onPlay(audio: Audio) {
audioPlayerConnector.playPauseAudio(audio)
}
override fun onProfileClick(userId: Int) { override fun onProfileClick(userId: Int) {
ProfileActivity.startActivity(requireContext(), userId) ProfileActivity.startActivity(requireContext(), userId)
} }

View File

@ -1,7 +0,0 @@
package com.isolaatti.home.notifications.presentation
import androidx.lifecycle.ViewModel
class NotificationsViewModel : ViewModel() {
// TODO: Implement the ViewModel
}

View File

@ -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)
}
}

View File

@ -1,4 +1,4 @@
package com.isolaatti.home.notifications.data package com.isolaatti.notifications.data
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET

View File

@ -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 import java.time.ZonedDateTime
data class NotificationsDto( data class NotificationsDto(
@ -18,5 +19,6 @@ data class NotificationPayload(
val type: String, val type: String,
val authorId: Int, val authorId: Int,
val authorName: String?, val authorName: String?,
val intentData: String? val intentData: String?,
val data: Map<String, String>
) )

View File

@ -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<Resource<List<Notification>>> = 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))
}
}
}

View File

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

View File

@ -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<Resource<List<Notification>>>
}

View File

@ -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<Notification, NotificationsAdapter.NotificationViewHolder>(
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<Notification>() {
override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean {
TODO("Not yet implemented")
}
override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean {
TODO("Not yet implemented")
}
}
}
}

View File

@ -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() {
}

View File

@ -1,4 +1,4 @@
package com.isolaatti.home.notifications.ui package com.isolaatti.notifications.ui
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import android.os.Bundle import android.os.Bundle
@ -7,7 +7,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.home.notifications.presentation.NotificationsViewModel import com.isolaatti.notifications.presentation.NotificationsViewModel
class NotificationsFragment : Fragment() { class NotificationsFragment : Fragment() {

View File

@ -2,6 +2,7 @@ package com.isolaatti.posting.posts.data.remote
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.isolaatti.audio.common.data.AudioDto
import java.io.Serializable import java.io.Serializable
data class FeedDto( data class FeedDto(
@ -23,7 +24,8 @@ data class FeedDto(
var numberOfComments: Int, var numberOfComments: Int,
val userName: String, val userName: String,
val squadName: String?, val squadName: String?,
var liked: Boolean var liked: Boolean,
var audio: AudioDto?
): Parcelable { ): Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
@ -32,7 +34,8 @@ data class FeedDto(
parcel.readInt(), parcel.readInt(),
parcel.readString()!!, parcel.readString()!!,
parcel.readString(), parcel.readString(),
parcel.readByte() != 0.toByte() parcel.readByte() != 0.toByte(),
parcel.readParcelable(AudioDto::class.java.classLoader)
) )
data class Post( data class Post(
@ -93,6 +96,7 @@ data class FeedDto(
parcel.writeString(userName) parcel.writeString(userName)
parcel.writeString(squadName) parcel.writeString(squadName)
parcel.writeByte(if (liked) 1 else 0) parcel.writeByte(if (liked) 1 else 0)
parcel.writeSerializable(audio)
} }
override fun describeContents(): Int { override fun describeContents(): Int {

View File

@ -54,7 +54,8 @@ data class Post(
numberOfLikes = it.numberOfLikes, numberOfLikes = it.numberOfLikes,
userName = it.userName, userName = it.userName,
squadName = it.squadName, squadName = it.squadName,
liked = it.liked liked = it.liked,
audio = it.audio?.let { audioDto -> Audio.fromDto(audioDto) }
) )
}.toMutableList() }.toMutableList()
} }

View File

@ -7,107 +7,149 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView 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
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load import coil.load
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.common.CoilImageLoader.imageLoader import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.common.OnUserInteractedWithPostCallback import com.isolaatti.common.OnUserInteractedWithPostCallback
import com.isolaatti.databinding.PostLayoutBinding
import com.isolaatti.posting.posts.domain.entity.Post import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.utils.UrlGen.userProfileImage import com.isolaatti.utils.UrlGen.userProfileImage
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callback: OnUserInteractedWithPostCallback) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){ class PostsRecyclerViewAdapter (
private val markwon: Markwon,
private val callback: OnUserInteractedWithPostCallback
) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){
private var postList: List<Post>? = null private var postList: List<Post>? = null
inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) { inner class FeedViewHolder(val itemBinding: PostLayoutBinding) : ViewHolder(itemBinding.root) {
fun bindView(postDto: Post, payloads: List<Any>) { fun bindView(post: Post, payloads: List<Any>) {
Log.d("payloads", payloads.count().toString())
val likeButton: MaterialButton = itemView.findViewById(R.id.like_button)
val commentsButton: MaterialButton = itemView.findViewById(R.id.comment_button)
if(payloads.isNotEmpty()) { if(payloads.isNotEmpty()) {
for(payload in payloads) { for(payload in payloads) {
when(payload) { when {
is LikeCountUpdatePayload -> { payload is LikeCountUpdatePayload -> {
itemBinding.likeButton.isEnabled = true
if(post.liked) {
likeButton.isEnabled = true itemBinding.likeButton.setIconTintResource(R.color.purple_lighter)
itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter))
if(postDto.liked) {
likeButton.setIconTintResource(R.color.purple_lighter)
likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter))
} else { } else {
likeButton.setIconTintResource(R.color.on_surface) itemBinding.likeButton.setIconTintResource(R.color.on_surface)
likeButton.setTextColor(itemView.context.getColor(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()
} }
payload is CommentsCountUpdatePayload -> {
is CommentsCountUpdatePayload -> { itemBinding.commentButton.text = post.numberOfComments.toString()
}
commentsButton.text = postDto.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 { } else {
val username: TextView = itemView.findViewById(R.id.text_view_username) val username: TextView = itemView.findViewById(R.id.text_view_username)
username.text = postDto.userName username.text = post.userName
username.setOnClickListener { username.setOnClickListener {
callback.onProfileClick(postDto.userId) callback.onProfileClick(post.userId)
} }
val profileImageView: ImageView = itemView.findViewById(R.id.avatar_picture) 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) 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) 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) { if(post.liked) {
likeButton.setIconTintResource(R.color.purple_lighter) itemBinding.likeButton.setIconTintResource(R.color.purple_lighter)
likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter)) itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter))
} else { } else {
likeButton.setIconTintResource(R.color.on_surface) itemBinding.likeButton.setIconTintResource(R.color.on_surface)
likeButton.setTextColor(itemView.context.getColor(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) val moreButton: MaterialButton = itemView.findViewById(R.id.more_button)
moreButton.setOnClickListener { moreButton.setOnClickListener {
callback.onOptions(postDto) callback.onOptions(post)
} }
likeButton.setOnClickListener { itemBinding.likeButton.setOnClickListener {
likeButton.isEnabled = false itemBinding.likeButton.isEnabled = false
if(postDto.liked){ if(post.liked){
callback.onUnLiked(postDto.id) callback.onUnLiked(post.id)
} else { } else {
callback.onLiked(postDto.id) callback.onLiked(post.id)
} }
} }
commentsButton.setOnClickListener { itemBinding.commentButton.setOnClickListener {
callback.onComment(postDto.id) callback.onComment(post.id)
} }
itemView.findViewById<MaterialCardView>(R.id.card).setOnClickListener { itemView.findViewById<MaterialCardView>(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 LikeCountUpdatePayload(val likeCount: Int)
data class CommentsCountUpdatePayload(val commentsCount: 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.post_layout, parent, false) return FeedViewHolder(PostLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false))
return FeedViewHolder(view)
} }
var previousSize = 0 var previousSize = 0
@ -197,4 +366,8 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba
} }
} }
companion object {
const val LOG_TAG = "PostsRecyclerViewAdapter"
}
} }

View File

@ -106,27 +106,57 @@ class ProfileMainFragment : Fragment() {
} }
} }
private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener { private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
override fun onPlaying(isPlaying: Boolean, audio: Playable) { 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) { override fun isLoading(isLoading: Boolean, audio: Playable) {
viewBinding.playButton.isEnabled = !isLoading if(audio == audioDescriptionAudio) {
viewBinding.audioProgress.isIndeterminate = isLoading viewBinding.playButton.isEnabled = !isLoading
viewBinding.audioProgress.isIndeterminate = isLoading
}
if(audio is Audio)
postsAdapter.setIsLoading(isLoading, audio)
} }
override fun progressChanged(second: Int, audio: Playable) { 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) { 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) { 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) //ProfileActivity.startActivity(requireContext(), userId)
} }
override fun onPlay(audio: Audio) {
audioPlayerConnector.playPauseAudio(audio)
}
override fun onLoadMore() { override fun onLoadMore() {
viewModel.getFeed(false) viewModel.getFeed(false)
} }
@ -474,5 +508,6 @@ class ProfileMainFragment : Fragment() {
companion object { companion object {
const val CALLER_ID = 30 const val CALLER_ID = 30
const val LOG_TAG = "ProfileMainFragment"
} }
} }

View File

@ -4,8 +4,13 @@ import android.Manifest
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.EntryPoint import dagger.hilt.EntryPoint
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.awaitResponse
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -19,7 +24,20 @@ class FcmService : FirebaseMessagingService() {
override fun onNewToken(token: String) { override fun onNewToken(token: String) {
pushNotificationsApi.registerDevice(token) CoroutineScope(Dispatchers.IO).launch {
Log.d(LOG_TAG, token) 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")
}
} }
} }

View File

@ -9,6 +9,6 @@ interface PushNotificationsApi {
@PUT("/api/push_notifications/register_device") @PUT("/api/push_notifications/register_device")
@Multipart @Multipart
fun registerDevice(@Part("token") token: String): Call<Any> fun registerDevice(@Part("token") token: String): Call<Unit>
} }

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/notification_main_image"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
tools:src="@tools:sample/avatars" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/notification_secondary_image"
android:layout_width="25dp"
android:layout_height="25dp"
app:layout_constraintBottom_toBottomOf="@+id/notification_main_image"
app:layout_constraintEnd_toEndOf="@+id/notification_main_image"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
tools:src="@drawable/baseline_image_24" />
<TextView
android:id="@+id/notification_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/notification_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@id/notification_main_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
android:layout_marginStart="16dp"
android:textStyle="bold"
android:textSize="18sp"
android:maxLines="1"
tools:text="Notification title" />
<TextView
android:id="@+id/notification_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@id/notification_main_image"
app:layout_constraintTop_toBottomOf="@+id/notification_title"
android:layout_marginStart="16dp"
android:maxLines="1"
tools:text="Notification message" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -56,6 +56,11 @@
android:gravity="end"/> android:gravity="end"/>
</RelativeLayout> </RelativeLayout>
<include android:id="@+id/audio"
layout="@layout/audio_attachment"
android:layout_marginHorizontal="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/post_content" android:id="@+id/post_content"

View File

@ -16,7 +16,7 @@
</fragment> </fragment>
<fragment <fragment
android:id="@+id/notificationsFragment" android:id="@+id/notificationsFragment"
android:name="com.isolaatti.home.notifications.ui.NotificationsFragment" android:name="com.isolaatti.notifications.ui.NotificationsFragment"
android:label="fragment_notifications" android:label="fragment_notifications"
tools:layout="@layout/fragment_notifications" > tools:layout="@layout/fragment_notifications" >
<action <action