Sharingang/Sharingang-Android

View on GitHub
app/src/main/java/com/example/sharingang/viewmodels/ItemsViewModel.kt

Summary

Maintainability
A
0 mins
Test Coverage
A
92%
package com.example.sharingang.viewmodels

import androidx.core.net.toUri
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import com.example.sharingang.database.repositories.ItemRepository
import com.example.sharingang.database.repositories.UserRepository
import com.example.sharingang.imagestore.ImageStore
import com.example.sharingang.models.Item
import com.example.sharingang.ui.adapters.ItemsAdapter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import javax.inject.Inject
import kotlin.collections.List
import kotlin.collections.Map
import kotlin.collections.asReversed
import kotlin.collections.filter
import kotlin.collections.forEach
import kotlin.collections.listOf
import kotlin.collections.set
import kotlin.collections.sortedWith
import kotlin.collections.toMutableMap

/**
 * ItemsViewModel models the state of the fragment for viewing items
 */
@HiltViewModel
class ItemsViewModel @Inject constructor(
    private val itemRepository: ItemRepository,
    private val userRepository: UserRepository,
    private val imageStore: ImageStore
) : ViewModel() {

    init {
        viewModelScope.launch {
            itemRepository.refreshItems()
        }
    }

    /**
     * Enum class representing all lists of the viewModel that can be observed.
     */
    enum class OBSERVABLES {
        ALL_ITEMS, SEARCH_RESULTS, USER_ITEMS_AND_REQUESTS, WISHLIST, ORDERED_ITEMS, USER_SOLD_BOUGHT
    }

    /**
     * Enum class representing all the ways to order a list of items.
     */
    enum class ORDERING {
        DATE, PRICE, NAME, CATEGORY
    }


    private val _userSoldAndBought = MutableLiveData<List<Item>>()
    val userSoldOrBought : LiveData<List<Item>>
        get() = _userSoldAndBought

    private val _navigateToDetailItem = MutableLiveData<Item?>()
    private val navigateToDetailItem: LiveData<Item?>
        get() = _navigateToDetailItem

    private val _refreshing = MutableLiveData(false)
    val refreshing: LiveData<Boolean>
        get() = _refreshing

    private val _searchResults = MutableLiveData<List<Item>>(listOf())
    val searchResults: LiveData<List<Item>>
        get() = _searchResults

    private val _orderedItems = MutableLiveData<List<Item>>(listOf())
    val orderedItemsResult: LiveData<List<Item>>
        get() = _orderedItems

    private val _userItemsAndRequests = MutableLiveData<List<Item>>()
    val userItemsAndRequests: LiveData<List<Item>>
        get() = _userItemsAndRequests

    private val _wishlistItem: MutableLiveData<List<Item>> = MutableLiveData(ArrayList())
    private val wishlistItem: LiveData<List<Item>>
        get() = _wishlistItem

    private val _reviews: MutableLiveData<Map<String, Boolean>> = MutableLiveData()
    val reviews: LiveData<Map<String, Boolean>>
        get() = _reviews

    /**
     * The last item created
     */
    val items: LiveData<List<Item>>
        get() = itemRepository.items()

    /**
     * Add or update an item.
     *
     * Add if it's id is null otherwise update it.
     *
     * @param item the item to be added / set
     * @param callback Will be called when finished with the item's id or null in case of error
     */
    fun setItem(item: Item, callback: ((String?) -> Unit)? = null) {
        viewModelScope.launch(Dispatchers.IO) {
            val uploadUrl = item.image?.let {
                if (!it.startsWith("https://") && !it.startsWith("http://")) {
                    imageStore.store(it.toUri())?.toString()
                } else {
                    it
                }
            }
            val itemId = itemRepository.set(item.copy(image = uploadUrl))
            if (callback != null) {
                viewModelScope.launch(Dispatchers.Main) {
                    callback(itemId)
                }
            }
        }
    }

    /**
     * Get all the items of the user, either their offers of requests depending on the
     * second parameter
     *
     * @param userId the id of the user
     * @param isRequest whether we should show the requests or the offers
     */
    fun getUserOffersAndRequests(userId: String?, isRequest: Boolean) {
        if (userId != null) {
            viewModelScope.launch(Dispatchers.IO) {
                _userItemsAndRequests.postValue(
                    itemRepository.userItems(userId)?.filter { item ->
                        !item.sold && isRequest == item.request
                    }
                )
            }
        }
    }

    /**
     * Function to update userSoldAndBought livedata
     *
     * @param[userId] the Id of the user.
     */
    fun updateSoldBought(userId: String?, soldWanted : Boolean){
        if(userId == null) return
        if(soldWanted){
            getUserSoldItems(userId)
        }else{
            updatePurchaseHistory(userId)
        }
    }

    /**
     * Get all items sold by a user.
     *
     * @param[userId] the id of the user.
     */
    fun getUserSoldItems(userId: String) {
        viewModelScope.launch(Dispatchers.IO) {
            _userSoldAndBought.postValue(
                itemRepository.userItems(userId)?.filter { item ->
                    item.sold
                }
            )
        }
    }

    /**
     * Function to retrieve all objects from user purchase history
     *
     * @param[userId] the Id of the user
     */
    private fun updatePurchaseHistory(userId: String){
        viewModelScope.launch(Dispatchers.IO){
            val user = userRepository.get(userId)
            if(user != null){
                val itemIdList = user.purchaseHistory
                val itemList = ArrayList<Item>()
                itemIdList.forEach {
                    val tempItem = itemRepository.get(it)
                    if(tempItem != null){
                        itemList.add(tempItem)
                    }
                }
                _userSoldAndBought.postValue(itemList)
            }
        }
    }


    /**
     * Clear the results of the search.
     */
    fun clearSearchResults() {
        _searchResults.value = listOf()
    }

    /**
     * Set the value of the wishlist.
     * @param[list] List of items to set as wishlist.
     */
    fun setWishList(list: List<Item>) {
        viewModelScope.launch {
            _wishlistItem.postValue(list)
        }
    }

    /**
     * Set the value of the reviews
     * @param[item] item whose reviews to use
     */
    fun setReviews(item: Item){
        _reviews.postValue(item.reviews)
    }

    /**
     * Searches through database, ignores searchName/
     * categoryID if null/empty.
     *
     * @param searchName string searched for
     * @param categoryID category searched for
     * @param onlyDiscounts if we search only discounted items
     */
    fun searchItems(searchName: String, categoryID: Int, onlyDiscounts: Boolean) {
        viewModelScope.launch(Dispatchers.IO) {
            val results = itemRepository.getAll().filter { item ->
                // If we have a category, it should match
                val matchCategory = categoryID == 0 || item.category == categoryID

                // If we have a search term, it should match
                val matchName = searchName.isEmpty() || item.title.lowercase()
                    .contains(searchName.lowercase())

                val matchDiscount = !onlyDiscounts || item.discount

                matchCategory && matchName && matchDiscount && !item.sold
            }
            _searchResults.postValue(results)
        }
    }

    /**
     * Order items in the database
     *
     * @param orderBy ORDERING
     * @param isAscending either ascending or descending order
     */
    fun orderItems(orderBy: ORDERING, isAscending: Boolean) {
        viewModelScope.launch(Dispatchers.IO) {
            var results = itemRepository.getAll().sortedWith(compareBy {
                when (orderBy) {
                    ORDERING.DATE -> it.createdAt
                    ORDERING.PRICE -> it.price
                    ORDERING.NAME -> it.title
                    ORDERING.CATEGORY -> it.category
                }
            })
            if (!isAscending) {
                results = results.asReversed()
            }
            _orderedItems.postValue(results)
        }
    }

    fun updateReview(item: Item, userId: String?, reviewed: Boolean) {
        if (userId == null) return
        viewModelScope.launch(Dispatchers.IO) {
            val newReviews = item.reviews.toMutableMap()
            newReviews[userId] = reviewed
            itemRepository.set(item.copy(reviews = newReviews))
            _reviews.postValue(newReviews)
        }
    }

    fun onViewItem(item: Item) {
        _navigateToDetailItem.value = item
    }

    private fun onViewItemNavigated() {
        _navigateToDetailItem.value = null
    }

    private suspend fun onSellItem(item: Item) {
        itemRepository.set(item.copy(sold = !item.sold))
    }

    /**
     * Sell an item.
     * @param[item] item to be sold.
     */
    suspend fun sellItem(item: Item?) {
        if (item != null) {
            onSellItem(item)
        }
    }

    /**
     * Add an observer to one of the observable lists of the view model.
     * @param[LifeCycleOwner]
     * @param[adapter] Adapter to be used to display items.
     * @param[type] The enum describing the list to be observed.
     */
    fun addObserver(LifeCycleOwner: LifecycleOwner, adapter: ItemsAdapter, type: OBSERVABLES) {
        val observable: LiveData<List<Item>> = when (type) {
            OBSERVABLES.ALL_ITEMS -> items
            OBSERVABLES.SEARCH_RESULTS -> searchResults
            OBSERVABLES.USER_ITEMS_AND_REQUESTS -> userItemsAndRequests
            OBSERVABLES.WISHLIST -> wishlistItem
            OBSERVABLES.ORDERED_ITEMS -> orderedItemsResult
            OBSERVABLES.USER_SOLD_BOUGHT -> userSoldOrBought
        }
        observable.observe(LifeCycleOwner, {
            adapter.submitList(it)
        })
    }

    /**
     * Sets up the navigation when clicking on items in a recycler view.
     *
     * @param[LifeCycleOwner]
     * @param[navController] Navigation controller used.
     * @param[actionDetail] The action to take on item click.
     */
    fun setupItemNavigation(
        LifeCycleOwner: LifecycleOwner,
        navController: NavController,
        actionDetail: (Item) -> NavDirections
    ) {
        navigateToDetailItem.observe(LifeCycleOwner, { item ->
            item?.let {
                navController.navigate(
                    actionDetail(item)
                )
                onViewItemNavigated()
            }
        })
    }

    /**
     * Update list of items, reads from database.
     */
    fun refresh() {
        _refreshing.value = true
        viewModelScope.launch {
            itemRepository.refreshItems()
            // Since we're in a coroutine, need to use post instead
            _refreshing.postValue(false)
        }
    }
}