conventionschedule-android/app/src/main/java/com/adlerosn/brasilfurfest/schedule/managers/ScheduleManager.kt

222 lines
9.2 KiB
Kotlin

package com.adlerosn.brasilfurfest.schedule.managers
import android.content.Context
import android.os.FileObserver
import android.util.Log
import com.adlerosn.brasilfurfest.helper.*
import com.adlerosn.brasilfurfest.helper.observables.Observable
import com.adlerosn.brasilfurfest.helper.observables.Observer
import com.adlerosn.brasilfurfest.notification.NotificationFirer
import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.convention.Announcement
import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.convention.Convention
import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.convention.ConventionSeries
import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.convention.Event
import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.managed.AttendeeConFavorites
import com.adlerosn.brasilfurfest.schedule.abstractDataTypes.managed.AttendeeConsFavorites
import com.google.gson.GsonBuilder
import org.jetbrains.anko.doAsync
import java.io.File
import java.io.Serializable
import java.util.*
class ScheduleManager(context: Context) : UncomplicatedObservable<Any?>(), Observer<Observable<Any?>, Any?>, Serializable {
override fun update(observable: Observable<Any?>, args: Any?) = onAttendeeIntentionsChanged()
private val uniqueIdentifierFile = File(context.filesDir, "rduid.txt")
val uniqueIdentifier: String
val cacheManager = CacheManager(context)
private val cachedChoices = File(context.filesDir, "schedule_choices.json")
private val cachedChoicesCanonicalPath = cachedChoices.canonicalPath
private val fireNotificationsClosure = { NotificationFirer().fire(context) }
private inner class InnerFileObserver(path: String, private val closure: ()->Any?) : FixedFileObserver(path){
override fun onEvent(event: Int, path: String) {
val mask = FileObserver.CLOSE_WRITE
if ((event and mask) != 0) {
closure()
}
}
}
private var diskChangesToNotLoad: Int = 0
private fun incrementDiskChangesToNotLoad(howMany: Int = 1){
synchronized(::diskChangesToNotLoad) {
diskChangesToNotLoad+=howMany
}
}
private fun notifyDiskChanges() {
synchronized(::diskChangesToNotLoad) {
if(diskChangesToNotLoad > 0)
diskChangesToNotLoad-=1
else
loadFromDisk()
}
setChanged()
notifyObservers()
}
private val fileObserver: InnerFileObserver
private val scheduleObserver: InnerFileObserver
fun updateConventionSeries() =
cacheManager[RemoteAssets.json.lastPathPart()]!!.first
.let { ConventionJsonReader.readFromInputStream(it) }
.also { conventionSeries = it }
var conventionSeries: ConventionSeries = updateConventionSeries()
val convention get() = conventionSeries.featured
val attendeeConventionsFavorites = AttendeeConsFavorites()
val attendeeFavorites
get(): AttendeeConFavorites {
lateinit var favorites: AttendeeConFavorites
synchronized(::attendeeConventionsFavorites) {
favorites = attendeeConventionsFavorites[convention]
}
return favorites
}
val conventionTime get() = GregorianCalendar().apply { timeInMillis = System.currentTimeMillis() } // .GregorianCalendar(2018,7,19,13,15)
private val jsonSerializer get() = GsonBuilder()
//.setExclusionStrategies(ObservableFieldsExclusionStrategy())
.registerTypeAdapter(Convention::class.java, ConventionInstanceCreator(convention))
.create()
// val nextNotificationTime: Long get(){
// val early: Long = convention.notificationFireMinutesBefore*60*1000.toLong()
// val now = conventionTime
// val thisRange = now.dayRange.split(convention.splitDayIn).first { conventionTime in it }
// val diff = thisRange.finish.timeInMillis - thisRange.start.timeInMillis
// val nextEnd = thisRange.start.timeInMillis+(1*diff)
// val nextNextEnd = thisRange.start.timeInMillis+(2*diff)
// val earlyNextEnd = nextEnd-early
// val earlyNextNextEnd = nextNextEnd-early
// return if(now.timeInMillis < earlyNextEnd) earlyNextEnd else earlyNextNextEnd
// }
init {
loadFromDisk()
if(!cachedChoices.exists())
saveToDisk(false)
uniqueIdentifier = initUniqueIdentifierFile()
fileObserver = (InnerFileObserver(cachedChoicesCanonicalPath) {notifyDiskChanges()}).apply { startWatching() }
scheduleObserver = (InnerFileObserver(cacheManager.getFileFor(RemoteAssets.json).absolutePath) {notifyScheduleChanged()}).apply { startWatching() }
}
private fun notifyScheduleChanged() {
conventionSeries = updateConventionSeries()
setChanged()
notifyObservers()
fireNotificationsClosure()
}
private val readAnnouncementsFile = File(context.filesDir, "announcements_read.txt")
val unreadAnnouncements: List<Announcement> get() {
if(!readAnnouncementsFile.exists())
return listOf()
val readAnnouncementUUIDs = readAnnouncementsFile.readLines()
return conventionSeries.announcements.filter { it.uuid !in readAnnouncementUUIDs }
}
fun setAnnouncementsRead() {
readAnnouncementsFile.writeText(
conventionSeries.announcements.map {
it.uuid
}.joinToString(separator = "\n")
)
}
private fun initUniqueIdentifierFile(): String{
if(!uniqueIdentifierFile.exists())
uniqueIdentifierFile.writeText(UUID.randomUUID().toString())
return uniqueIdentifierFile.readText()
}
private fun readObject(ois: java.io.ObjectInputStream){
ois.defaultReadObject()
fileObserver.stopWatching()
fileObserver.startWatching()
scheduleObserver.stopWatching()
scheduleObserver.startWatching()
loadFromDisk()
}
private fun loadFromDisk() {
if (cachedChoices.exists()) {
synchronized(::attendeeConventionsFavorites) {
val json = cachedChoices.readText()
val stateFromDisk = try {
jsonSerializer.fromJson(json, attendeeConventionsFavorites::class.java)
} catch (e: com.google.gson.JsonSyntaxException) {
Log.e("Schedule manager", "Malformed JSON", e)
AttendeeConsFavorites()
}
if (stateFromDisk != null) {
stateFromDisk.recursivelyRemoveObservers()
attendeeConventionsFavorites.recursivelyRemoveObservers()
attendeeConventionsFavorites.clear()
attendeeConventionsFavorites.putAll(stateFromDisk)
attendeeConventionsFavorites.observeAllChildren()
attendeeConventionsFavorites.addObserver(this)
}
}
}
}
private fun saveToDisk(async: Boolean = true) {
synchronized(::attendeeConventionsFavorites) {
attendeeConventionsFavorites.recursivelyRemoveObservers()
val json = jsonSerializer.toJson(attendeeConventionsFavorites)
attendeeConventionsFavorites.observeAllChildren()
attendeeConventionsFavorites.addObserver(this)
incrementDiskChangesToNotLoad()
val closure = { cachedChoices.bufferedWriter().use { it.write(json) } }
if(async)
doAsync { closure() }
else
closure()
}
}
fun onAttendeeIntentionsChanged() {
saveToDisk()
}
fun clearIntentions() {
attendeeConventionsFavorites.clear()
saveToDisk()
}
val currentSplashAsset: String get() = (
conventionSeries.banners.firstOrNull { conventionTime in it.lifetime }?.banner
?: conventionSeries.defaultBanner
).solved!!
val editionHasStarted: Boolean get() = conventionTime.timeInMillis > convention.officialLifespan.start.timeInMillis
val editionHasEnded: Boolean get() = conventionTime.timeInMillis > convention.officialLifespan.finish.timeInMillis
val nextEditionStartTime : Long? get(){
val currentTime = conventionTime.timeInMillis
val timeUntilThis = convention.officialLifespan.start.timeInMillis - currentTime
val timeUntilNext = (convention.nextEdition?.timeInMillis ?: Long.MAX_VALUE) - currentTime
val stillHappening = editionHasStarted and !editionHasEnded
return when {
timeUntilThis > 0 -> convention.officialLifespan.start.timeInMillis
stillHappening -> null
timeUntilNext > 0 -> convention.nextEdition?.timeInMillis
else -> null
}
}
val nextEditionStartCountdown : Triple<Int, Int, Int>?
get() = nextEditionStartTime?.let { buildDayHourMinuteCountdownTriple(it - conventionTime.timeInMillis) }
private fun buildDayHourMinuteCountdownTriple(millisUntil: Long): Triple<Int, Int, Int>{
return (millisUntil/1000).let {
Triple((it/(3600*24)).toInt(), ((it/3600)%24).toInt(), ((it/60)%60).toInt())
}
}
fun canAttend(event: Event) = attendeeFavorites.canAttend(event)
}