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
targetSdk 34
versionCode 1
versionName "1.0"
versionName "0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

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

View File

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

View File

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

View File

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

View File

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

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.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
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<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 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() {

View File

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

View File

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

View File

@ -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<PostsRecyclerViewAdapter.FeedViewHolder>(){
class PostsRecyclerViewAdapter (
private val markwon: Markwon,
private val callback: OnUserInteractedWithPostCallback
) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){
private var postList: List<Post>? = null
inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) {
fun bindView(postDto: 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)
inner class FeedViewHolder(val itemBinding: PostLayoutBinding) : ViewHolder(itemBinding.root) {
fun bindView(post: Post, payloads: List<Any>) {
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()
}
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)
}
is CommentsCountUpdatePayload -> {
commentsButton.text = postDto.numberOfComments.toString()
}
}
}
} 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<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 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"
}
}

View File

@ -106,29 +106,59 @@ 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) {
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) {
if(audio == audioDescriptionAudio) {
viewBinding.audioProgress.setProgress(second, true)
}
if(audio is Audio)
postsAdapter.setProgress(second, audio)
}
override fun durationChanged(duration: Int, audio: Playable) {
if(audio == audioDescriptionAudio) {
viewBinding.audioProgress.max = duration
}
if(audio is Audio)
postsAdapter.setDuration(duration, audio)
}
override fun onEnded(audio: Playable) {
if(audio == audioDescriptionAudio) {
viewBinding.audioProgress.progress = 0
}
if(audio is Audio)
postsAdapter.setEnded(audio)
}
}
private val profileObserver = Observer<UserProfile> { profile ->
@ -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"
}
}

View File

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

View File

@ -9,6 +9,6 @@ interface PushNotificationsApi {
@PUT("/api/push_notifications/register_device")
@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"/>
</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
android:id="@+id/post_content"

View File

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