SDPTeam15/PolyEvents

View on GitHub
app/src/main/java/com/github/sdpteam15/polyevents/view/fragments/EventListFragment.kt

Summary

Maintainability
A
0 mins
Test Coverage
A
94%
package com.github.sdpteam15.polyevents.view.fragments

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.github.sdpteam15.polyevents.R
import com.github.sdpteam15.polyevents.helper.HelperFunctions
import com.github.sdpteam15.polyevents.helper.HelperFunctions.showToast
import com.github.sdpteam15.polyevents.helper.NotificationsHelper
import com.github.sdpteam15.polyevents.helper.NotificationsScheduler
import com.github.sdpteam15.polyevents.model.database.local.entity.EventLocal
import com.github.sdpteam15.polyevents.model.database.local.room.LocalDatabase
import com.github.sdpteam15.polyevents.model.database.remote.Database.currentDatabase
import com.github.sdpteam15.polyevents.model.database.remote.DatabaseConstant
import com.github.sdpteam15.polyevents.model.entity.Event
import com.github.sdpteam15.polyevents.model.observable.Observable
import com.github.sdpteam15.polyevents.model.observable.ObservableList
import com.github.sdpteam15.polyevents.view.PolyEventsApplication
import com.github.sdpteam15.polyevents.view.activity.EventActivity
import com.github.sdpteam15.polyevents.view.adapter.EventItemAdapter
import com.github.sdpteam15.polyevents.viewmodel.EventLocalViewModel
import com.github.sdpteam15.polyevents.viewmodel.EventLocalViewModelFactory
import com.google.android.material.switchmaterial.SwitchMaterial
import java.time.LocalDateTime

/**
 * Extra containing the event ID to show on the launched event page
 */
const val EXTRA_EVENT_ID = "com.github.sdpteam15.polyevents.event.EVENT_ID"
const val EXTRA_EVENT_NAME = "com.github.sdpteam15.polyevents.event.EVENT_NAME"

/**
 * Shows the list of events and displays them in a new event when we click on one of them
 */
class EventListFragment : Fragment() {

    companion object {
        const val TAG = "EventListFragment"

        // for testing purposes
        lateinit var localDatabase: LocalDatabase
        lateinit var eventLocalViewModel: EventLocalViewModel
    }

    private lateinit var recyclerView: RecyclerView
    private lateinit var myEventsSwitch: SwitchMaterial
    private lateinit var noUpcomingEventsTextView: TextView

    val events = ObservableList<Event>()

    // events observable will be sorted on start time
    private lateinit var eventsDB: ObservableList<Event>

    // observable for events stored in local cache
    private val eventsLocal = ObservableList<EventLocal>()

    // Flag to check if we fetched events data successfully from remote
    private var fetchedDataFromRemote = false

    lateinit var notificationsScheduler: NotificationsScheduler

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val fragmentView = inflater.inflate(R.layout.fragment_events, container, false)

        localDatabase = (requireActivity().application as PolyEventsApplication).localDatabase
        eventLocalViewModel = EventLocalViewModelFactory(
            localDatabase.eventDao()
        ).create(
            EventLocalViewModel::class.java
        )

        notificationsScheduler = NotificationsHelper(requireActivity().applicationContext)

        eventsDB = events.sortAndLimitFrom(requireActivity()) {
            it.startTime
        }

        recyclerView = fragmentView.findViewById(R.id.recycler_events_list)
        noUpcomingEventsTextView =
            fragmentView.findViewById(R.id.fragment_events_no_upcoming_events_text_view)

        myEventsSwitch = fragmentView.findViewById(R.id.event_list_my_events_switch)
        myEventsSwitch.setOnCheckedChangeListener { _, isChecked ->
            myEventsSwitchCallback(isChecked)
        }

        val openEvent = { event: Event ->
            val intent = Intent(inflater.context, EventActivity::class.java).apply {
                putExtra(EXTRA_EVENT_ID, event.eventId)
            }
            startActivity(intent)
        }

        recyclerView.adapter = EventItemAdapter(events, openEvent)
        recyclerView.setHasFixedSize(false)

        updateEventsList()
        // Inflate the layout for this fragment
        return fragmentView
    }

    override fun onResume() {
        super.onResume()

        myEventsSwitchCallback(myEventsSwitch.isChecked)
    }

    /**
     * Retrieve my events if switch is on, else retrieve all events from remote database
     * @param isChecked true if the switch is on to retrieve only the events the current user $
     * if registered to from the local database
     */
    private fun myEventsSwitchCallback(isChecked: Boolean) {
        if (isChecked) {
            getUserLocalSubscribedEvents()
        } else {
            getEventsListAndDisplay(context)
        }
    }

    /**
     * Get all the events the current user is subscribed to from the local cache (room database).
     */
    private fun getUserLocalSubscribedEvents() {
        eventLocalViewModel.getAllEvents(eventsLocal)

        eventsLocal.observe(this, update = false) {
            var retrievedEvents = it.value
            if (fetchedDataFromRemote) {
                // Sync with remote events
                retrievedEvents = syncLocalWithFetchedEventsFromRemote(retrievedEvents)
                fetchedDataFromRemote = false
            }
            // Sync events local with remote
            eventsDB.updateAll(retrievedEvents.map { it.toEvent() })
        }
    }

    /**
     * Synchronize the events stored in local cache with those stored in the remote database.
     * For each event in the local cache, check if the remote database's events contain such an events.
     * If an event is no longer on remote (e.g. deleted), delete it from cache as well
     * and update the list.
     * @param eventsInLocal the list of events stored in local cache to synchronize
     * @return the list of events stored in local cache filtered of obsolete events
     */
    private fun syncLocalWithFetchedEventsFromRemote(eventsInLocal: List<EventLocal>): List<EventLocal> {
        val eventsLocalCopy = eventsInLocal.toMutableList()
        var eventsChanged = false

        for (eventLocal in eventsInLocal) {
            /*
            eventId uniquely identifies an event. If no event exists in the remote with that event
            id, then that event has been deleted, and must be deleted in the local cache as well
             */
            if (!events.any {
                    it.eventId == eventLocal.eventId
                }) {
                notificationsScheduler.cancelNotification(eventLocal.eventStartNotificationId)
                notificationsScheduler.cancelNotification(eventLocal.eventBeforeNotificationId)
                eventLocalViewModel.delete(eventLocal)
                eventsLocalCopy.remove(eventLocal)
                eventsChanged = true
            }
        }

        if (eventsChanged) {
            eventsLocal.updateAll(eventsLocalCopy)
            showToast(getString(R.string.events_local_sync_remote), context)
        }
        return eventsLocalCopy
    }

    /**
     * Get event list from database and display it on the fragment
     * @param context The current application context
     */
    private fun getEventsListAndDisplay(context: Context?) {
        val observableDBAnswer = Observable<Boolean>()

        currentDatabase.eventDatabase.getEvents(eventsDB, matcher = { collection ->
            // Get only events that are not finished yet...
            collection.whereGreaterThanOrEqualTo(
                DatabaseConstant.EventConstant.EVENT_END_TIME.value,
                HelperFunctions.localDateTimeToDate(LocalDateTime.now())!!
            )
        }).observe(requireActivity()) {
            if (!it.value) {
                showToast(getString(R.string.fail_to_get_information), context)
            } else {
                fetchedDataFromRemote = true
            }
        }.then.updateOnce(requireActivity(), observableDBAnswer)

        HelperFunctions.showProgressDialog(
            requireActivity(), listOf(
                observableDBAnswer
            ), requireActivity().supportFragmentManager
        )
    }

    /**
     * Update the event list
     */
    private fun updateEventsList() {
        events.observe(this) {
            recyclerView.adapter!!.notifyDataSetChanged()
            if (it.value.isEmpty()) {
                noUpcomingEventsTextView.visibility = View.VISIBLE
            } else {
                noUpcomingEventsTextView.visibility = View.INVISIBLE
            }
        }
    }
}