LarryHsiao/Nyx

View on GitHub
app/src/main/java/com/larryhsiao/nyx/jot/JotViewModel.kt

Summary

Maintainability
C
7 hrs
Test Coverage
package com.larryhsiao.nyx.jot

import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.larryhsiao.clotho.openweather.JsonWeather
import com.larryhsiao.clotho.openweather.CurrentWeather
import com.larryhsiao.clotho.openweather.Weather
import com.larryhsiao.nyx.BuildConfig.OPEN_WEATHER_API_KEY
import com.larryhsiao.nyx.core.util.StringHashTags
import com.larryhsiao.nyx.core.attachments.*
import com.larryhsiao.nyx.core.jots.*
import com.larryhsiao.nyx.core.metadata.Metadata.Type.OPEN_WEATHER
import com.larryhsiao.nyx.core.metadata.openweather.PostedWeatherMeta
import com.larryhsiao.nyx.core.tags.*
import com.larryhsiao.clotho.Action
import com.larryhsiao.clotho.source.ConstSource
import com.larryhsiao.nyx.core.Nyx
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.Double.Companion.MIN_VALUE
import kotlin.collections.ArrayList

/**
 * ViewModel to representing a jot.
 */
class JotViewModel(
    private val nyx: Nyx,
    private val localFileSync: Action
) : ViewModel() {
    companion object {
        private const val TAG_DETECTION_DELAY_SEC = 1
    }

    private var secondsAfterContentChanged = Int.MAX_VALUE

    init {
        viewModelScope.launch(IO) {
            while (isActive) {
                if (secondsAfterContentChanged == TAG_DETECTION_DELAY_SEC) {
                    preferTags()
                }
                secondsAfterContentChanged++
                delay(1000)
            }
        }
    }

    private val time = MutableLiveData<Long>()
    fun time(): LiveData<Long> = time

    private val content = MutableLiveData<String>()
    fun content(): LiveData<String> = content

    private val title = MutableLiveData<String>()
    fun title(): LiveData<String> = title

    private val location = MutableLiveData<DoubleArray>()
    fun location(): LiveData<DoubleArray> = location

    private val jot = MutableLiveData<Jot>()

    private val isNewJotLiveData = MutableLiveData<Boolean>()
    fun isNewJot(): LiveData<Boolean> = isNewJotLiveData

    private val attachments = MutableLiveData<List<String>>()
    fun attachments(): LiveData<List<String>> = attachments

    private val isModified = MutableLiveData<Boolean>()
    fun isModified(): LiveData<Boolean> = isModified

    private val weather = MutableLiveData<Weather?>()
    fun weather(): LiveData<Weather?> = weather

    private val tags: MutableMap<String, Tag> = HashMap<String, Tag>()
    private val tagsLiveData = MutableLiveData<Map<String, Tag>>().apply {
        value = tags
    }

    private val privateLock = MutableLiveData<Boolean>()
    fun privateLock(): LiveData<Boolean> = privateLock

    fun tags(): LiveData<Map<String, Tag>> = tagsLiveData

    fun loadJot(id: Long) = viewModelScope.launch(IO) {
        if (id == -1L) {
            newJot(Calendar.getInstance())
        } else {
            isNewJotLiveData.postValue(false)
            val jot = nyx.jots().byId(id)
            loadContent(jot)
            loadAttachments(jot)
            loadWeather(jot)
            loadTags(jot)
        }
    }

    private fun loadTags(jot: Jot) {
        tagsLiveData.postValue(
            nyx.tags().byJotId(jot.id())
                .map { it.title() to it }
                .toMap()
        )
    }

    private fun loadWeather(jot: Jot) {
        val filter = nyx.metadataSet().byJotId(jot.id()).filter { it.type() == OPEN_WEATHER }

        if (filter.isNotEmpty()) {
            weather.postValue(
                JsonWeather(
                    filter[0].value()
                )
            )
        }
    }

    fun newJot(date: Calendar) = viewModelScope.launch(IO) {
        isNewJotLiveData.postValue(true)
        loadContent(object : WrappedJot(ConstJot()) {
            override fun createdTime(): Long {
                return date.timeInMillis
            }
        })
    }

    private fun loadContent(newJot: Jot) = viewModelScope.launch {
        jot.value = newJot
        time.value = newJot.createdTime()
        content.value = newJot.content()
        title.value = newJot.title()
        location.value = newJot.location()
        privateLock.value = newJot.privateLock()
    }

    private fun loadAttachments(jot: Jot) = viewModelScope.launch(IO) {
        attachments.postValue(
            nyx.attachments().byJotId(jot.id()).map { it.uri() }
        )
    }

    suspend fun save() = withContext(IO) {
        preferTags()
        val savedJot = nyx.jots().update(object : WrappedJot(jot.value ?: ConstJot()) {
            override fun content() = content.value ?: ""
            override fun title() = title.value ?: ""
            override fun createdTime() = time.value ?: 0L
            override fun location() = location.value ?: doubleArrayOf(MIN_VALUE, MIN_VALUE)
            override fun privateLock() = privateLock.value ?: false
        })
        saveAttachments(savedJot)
        saveWeather(savedJot)
        saveTags(savedJot)
    }

    private fun saveTags(jot: Jot) {
        nyx.jotTags().deleteByJotId(jot.id())
        tags.values.forEach { selected ->
            val tag = nyx.tags().create(selected.title())
            nyx.jotTags().link(
                ConstSource(jot.id()).value(),
                ConstSource(tag.id()).value()
            )
        }
    }

    private fun saveWeather(savedJot: Jot) {
        if (weather.value == null) {
            nyx.metadataSet().weathers().removeByJotId(savedJot.id())
        } else {
            weather.value?.let {
                nyx.metadataSet().weathers().update(savedJot.id(), it)
            }
        }
    }

    private fun saveAttachments(jot: Jot) {
        val exists = nyx.attachments().byJotId(jot.id())
        val new = (attachments.value ?: emptyList()).map { it }.toHashSet()
        val delete = ArrayList<Attachment>()
        exists.forEach { exist ->
            if (!new.contains(exist.uri())) {
                delete.add(exist)
            }
            new.remove(exist.uri())
        }
        new.forEach {
            nyx.attachments().newAttachment(ConstAttachment(-1, jot.id(), it, 0, 0))
        }
        delete.forEach { nyx.attachments().deleteById(it.id()) }
        localFileSync.fire()
    }

    fun preferTitle(newTitle: String) {
        if (title.value == newTitle) {
            return
        }
        title.value = newTitle
        markModified()
    }

    fun preferContent(newContent: String) {
        if (content.value == newContent) {
            return
        }
        content.value = newContent
        secondsAfterContentChanged = 0
        markModified()
    }

    private fun preferTags() {
        val newTagsMap = StringHashTags(
            content.value ?: ""
        ).value().map {
            // In-memory tag object, we actually create tag when we saving it.
            it to ConstTag(-1, it)
        }.toMap()
        if (newTagsMap.keys.toTypedArray().contentEquals(tags.keys.toTypedArray())) {
            return
        }
        tags.filter { newTagsMap.containsKey(it.key).not() }
            .forEach { deleted -> tags.remove(deleted.key) }
        tags.putAll(newTagsMap)
        tagsLiveData.postValue(tags)
    }

    fun preferTime(newTime: Long) {
        time.value = newTime
        updateWeather()
        markModified()
    }

    fun preferLocation(newLocation: DoubleArray) {
        location.value = newLocation
        updateWeather()
        markModified()
    }

    fun togglePrivateContent() {
        privateLock.value = !(privateLock.value ?: false)
        markModified()
    }

    private fun updateWeather() {
        val selectLocation = location.value
        if (selectLocation != null && weather.value == null &&
            !selectLocation.contentEquals(doubleArrayOf(MIN_VALUE, MIN_VALUE)) &&
            dayCalendar(time.value ?: System.currentTimeMillis()).timeInMillis ==
            dayCalendar(System.currentTimeMillis()).timeInMillis
        ) {
            loadWeather(selectLocation[0], selectLocation[1])
        } else {
            weather.value = null
        }
    }

    private fun loadWeather(longitude: Double, latitude: Double) = viewModelScope.launch {
        val result = withContext(IO) {
            CurrentWeather(
                OPEN_WEATHER_API_KEY,
                latitude,
                longitude
            ).value()
        }
        weather.value = result
    }

    fun preferAttachments(newAttachments: List<Uri>) {
        attachments.value = newAttachments.map { it.toString() }
        markModified()
    }

    suspend fun delete() = withContext(IO) {
        nyx.jots().deleteById(jot.value?.id() ?: -1)
    }

    private fun markModified() {
        if (isModified.value != true) {
            isModified.value = true
        }
    }

    private fun dayCalendar(time: Long): Calendar {
        return Calendar.getInstance().apply {
            timeInMillis = time
            set(Calendar.HOUR_OF_DAY, 0)
            set(Calendar.MINUTE, 0)
            set(Calendar.SECOND, 0)
            set(Calendar.MILLISECOND, 0)
        }
    }
}