[Timetable] Implement lazy day loading. Introduce TimetableManager class.

This commit is contained in:
Kuba Szczodrzyński 2020-03-10 19:27:18 +01:00
parent 06011bf4ae
commit acd5e9b998
10 changed files with 95 additions and 58 deletions

View File

@ -47,6 +47,7 @@ import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity
import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
import pl.szczodrzynski.edziennik.utils.managers.NotificationChannelsManager
import pl.szczodrzynski.edziennik.utils.managers.TimetableManager
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
@ -67,6 +68,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
val userActionManager by lazy { UserActionManager(this) }
val gradesManager by lazy { GradesManager(this) }
val timetableManager by lazy { TimetableManager(this) }
val db
get() = App.db

View File

@ -5,10 +5,9 @@
package pl.szczodrzynski.edziennik.data.db.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
@ -53,6 +52,9 @@ interface TimetableDao {
@Query("DELETE FROM timetable WHERE profileId = :profileId AND type != -1 AND ((type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo))")
fun clearBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date)
@RawQuery(observedEntities = [Lesson::class])
fun getRaw(query: SupportSQLiteQuery): LiveData<List<LessonFull>>
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND type != -1 AND type != 0
@ -67,12 +69,11 @@ interface TimetableDao {
""")
fun getChangesForDateNow(profileId: Int, date: Date): List<LessonFull>
@Query("""
fun getForDate(profileId: Int, date: Date) = getRaw(SimpleSQLiteQuery("""
$QUERY
WHERE timetable.profileId = :profileId AND ((type != 3 AND date = :date) OR ((type = 3 OR type = 1) AND oldDate = :date))
WHERE timetable.profileId = $profileId AND ((type != 3 AND date = "${date.stringY_m_d}") OR ((type = 3 OR type = 1) AND oldDate = "${date.stringY_m_d}"))
ORDER BY id, type
""")
fun getForDate(profileId: Int, date: Date): LiveData<List<LessonFull>>
"""))
@Query("""
$QUERY

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.db.entity
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
@ -66,6 +67,9 @@ open class Lesson(val profileId: Int, var id: Long) {
fun buildId(): Long = (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + (hashCode() and 0xFFFF)
@Ignore
var showAsUnseen = false
override fun toString(): String {
return "Lesson(profileId=$profileId, " +
"id=$id, " +

View File

@ -25,7 +25,6 @@ import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableUtils
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
@ -50,7 +49,7 @@ class LessonDetailsDialog(
get() = job + Dispatchers.Main
private lateinit var adapter: EventListAdapter
private val utils by lazy { TimetableUtils() }
private val manager by lazy { app.timetableManager }
init { run {
if (activity.isFinishing)
@ -91,7 +90,7 @@ class LessonDetailsDialog(
val lessonTime = lesson.displayStartTime ?: return
b.lessonDate.text = Week.getFullDayName(lessonDate.weekDay) + ", " + lessonDate.formattedString
b.annotationVisible = utils.getAnnotation(activity, lesson, b.annotation)
b.annotationVisible = manager.getAnnotation(activity, lesson, b.annotation)
if (lesson.type >= Lesson.TYPE_SHIFTED_SOURCE) {
b.shiftedLayout.visibility = View.VISIBLE

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-22.
*/
package pl.szczodrzynski.edziennik.ui.modules.base
import androidx.fragment.app.Fragment
abstract class PagerFragment : Fragment() {
private var isPageCreated = false
/**
* Called when the page is first shown, or if previous
* [onPageCreated] returned false
*
* @return true if the view is set up
* @return false if the setup failed. The method may be then called
* again, when page becomes visible.
*/
abstract fun onPageCreated(): Boolean
override fun onResume() {
if (!isPageCreated) {
isPageCreated = onPageCreated()
}
super.onResume()
}
}

View File

@ -5,15 +5,15 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.linkedin.android.tachyon.DayView
import com.linkedin.android.tachyon.DayViewConfig
@ -27,6 +27,7 @@ import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding
import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.base.PagerFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR
import pl.szczodrzynski.edziennik.utils.ListenerScrollView
@ -35,7 +36,7 @@ import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
class TimetableDayFragment : Fragment(), CoroutineScope {
class TimetableDayFragment : PagerFragment(), CoroutineScope {
companion object {
private const val TAG = "TimetableDayFragment"
}
@ -44,7 +45,7 @@ class TimetableDayFragment : Fragment(), CoroutineScope {
private lateinit var activity: MainActivity
private lateinit var inflater: AsyncLayoutInflater
private lateinit var job: Job
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
@ -53,7 +54,7 @@ class TimetableDayFragment : Fragment(), CoroutineScope {
private var endHour = DEFAULT_END_HOUR
private var firstEventMinute = 24 * 60
private val utils by lazy { TimetableUtils() }
private val manager by lazy { app.timetableManager }
// find SwipeRefreshLayout in the hierarchy
private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) }
@ -88,25 +89,23 @@ class TimetableDayFragment : Fragment(), CoroutineScope {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
if (context == null)
return null
context ?: return null
app = activity.application as App
job = Job()
this.inflater = AsyncLayoutInflater(context!!)
date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday()
startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR
endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR
return FrameLayout(activity).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
addView(ProgressBar(activity).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)
})
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
Log.d(TAG, "onViewCreated, date=$date")
override fun onPageCreated(): Boolean {
if (!isAdded)
return false
// observe lesson database
app.db.timetableDao().getForDate(App.profileId, date).observe(this, Observer { lessons ->
@ -117,6 +116,8 @@ class TimetableDayFragment : Fragment(), CoroutineScope {
processLessonList(lessons, events)
}
})
return true
}
private fun processLessonList(lessons: List<LessonFull>, events: List<EventFull>) {
@ -174,6 +175,8 @@ class TimetableDayFragment : Fragment(), CoroutineScope {
}
dayView.setHourLabelViews(hourLabelViews)
lessons.forEach { it.showAsUnseen = !it.seen }
buildLessonViews(lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }, events)
}
@ -209,7 +212,6 @@ class TimetableDayFragment : Fragment(), CoroutineScope {
eventView.tag = lesson
eventView.setOnClickListener {
Log.d(TAG, "Clicked ${it.tag}")
if (isAdded && it.tag is LessonFull)
LessonDetailsDialog(activity, it.tag as LessonFull)
}
@ -275,10 +277,13 @@ class TimetableDayFragment : Fragment(), CoroutineScope {
lb.detailsFirst.text = listOfNotEmpty(timeRange, classroomInfo).concat(bullet)
lb.detailsSecond.text = listOfNotEmpty(teacherInfo, teamInfo).concat(bullet)
lb.unread = lesson.type != Lesson.TYPE_NORMAL && !lesson.seen
lb.unread = lesson.type != Lesson.TYPE_NORMAL && lesson.showAsUnseen
if (!lesson.seen) {
manager.markAsSeen(lesson)
}
//lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
lb.annotationVisible = utils.getAnnotation(activity, lesson, lb.annotation)
lb.annotationVisible = manager.getAnnotation(activity, lesson, lb.annotation)
// The day view needs the event time ranges in the start minute/end minute format,
// so calculate those here

View File

@ -15,7 +15,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.viewpager.widget.ViewPager
import com.google.android.material.datepicker.MaterialDatePicker
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
@ -26,12 +25,9 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
import pl.szczodrzynski.edziennik.observeOnce
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.dialogs.timetable.GenerateBlockTimetableDialog
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
@ -50,7 +46,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
private lateinit var activity: MainActivity
private lateinit var b: FragmentTimetableV2Binding
private lateinit var job: Job
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
@ -61,11 +57,6 @@ class TimetableFragment : Fragment(), CoroutineScope {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
job = Job()
context!!.theme.applyStyle(Themes.appTheme, true)
if (app.profile == null)
return inflater.inflate(R.layout.fragment_loading, container, false)
// activity, context and profile is valid
b = FragmentTimetableV2Binding.inflate(inflater)
// TODO: 2020-01-05 resolve issues with page scrolling (and scrolling up) with viewpager and swipe to refresh
//b.refreshLayout.setParent(activity.swipeRefreshLayout)
@ -91,8 +82,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { launch {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
if (!isAdded)
return@launch
if (app.profile.getStudentData("timetableNotPublic", false)) {
@ -141,7 +131,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
startHour,
endHour
)
b.viewPager.offscreenPageLimit = 2
b.viewPager.offscreenPageLimit = 1
b.viewPager.adapter = pagerAdapter
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
@ -162,7 +152,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
activity.gainAttentionFAB()
fabShown = true
}
markLessonsAsSeen()
//markLessonsAsSeen()
}
})
@ -224,7 +214,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
})
}}
private fun markLessonsAsSeen() = pageSelection?.let { date ->
/*private fun markLessonsAsSeen() = pageSelection?.let { date ->
app.db.timetableDao().getForDate(App.profileId, date).observeOnce(this@TimetableFragment, Observer { lessons ->
lessons.forEach { lesson ->
if (lesson.type != Lesson.TYPE_NORMAL && lesson.type != Lesson.TYPE_NO_LESSONS
@ -233,5 +223,5 @@ class TimetableFragment : Fragment(), CoroutineScope {
}
}
})
}
}*/
}

View File

@ -33,11 +33,6 @@ class TimetablePagerAdapter(
putInt("endHour", endHour)
}
}
/*return TimetableDayFragment().apply {
arguments = Bundle().also {
it.putLong("date", items[position].value.toLong())
}
}*/
}
override fun getCount(): Int {

View File

@ -1,22 +1,35 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-16.
* Copyright (c) Kuba Szczodrzyński 2020-3-10.
*/
package pl.szczodrzynski.edziennik.ui.modules.timetable
package pl.szczodrzynski.edziennik.utils.managers
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.widget.TextView
import pl.szczodrzynski.edziennik.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.setText
import pl.szczodrzynski.edziennik.setTintColor
import pl.szczodrzynski.navlib.getColorFromAttr
import kotlin.coroutines.CoroutineContext
class TimetableManager(val app: App) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
fun markAsSeen(lesson: LessonFull) {
lesson.seen = true
startCoroutineTimer(500L, 0L) {
app.db.metadataDao().setSeen(lesson.profileId, lesson, true)
}
}
class TimetableUtils {
fun getAnnotation(context: Context, lesson: LessonFull, annotation: TextView): Boolean {
var annotationVisible = false
when (lesson.type) {

View File

@ -74,8 +74,8 @@
tools:text="pracownia urządzeń techniki komputerowej" />
<View
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"