Display day header from website in timetable (#1269)

This commit is contained in:
Mikołaj Pich 2021-04-05 15:07:29 +02:00 committed by GitHub
parent 7bc5219d81
commit aeb3b2a030
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2497 additions and 71 deletions

View File

@ -160,7 +160,7 @@ ext {
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:1.1.3" implementation "io.github.wulkanowy:sdk:877b9135"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'

File diff suppressed because it is too large Load Diff

View File

@ -181,4 +181,8 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideStudentInfoDao(database: AppDatabase) = database.studentInfoDao fun provideStudentInfoDao(database: AppDatabase) = database.studentInfoDao
@Singleton
@Provides
fun provideTimetableHeaderDao(database: AppDatabase) = database.timetableHeaderDao
} }

View File

@ -32,6 +32,7 @@ import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.dao.TeacherDao import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.CompletedLesson
@ -58,6 +59,7 @@ import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Teacher import io.github.wulkanowy.data.db.entities.Teacher
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.db.migrations.Migration10 import io.github.wulkanowy.data.db.migrations.Migration10
import io.github.wulkanowy.data.db.migrations.Migration11 import io.github.wulkanowy.data.db.migrations.Migration11
import io.github.wulkanowy.data.db.migrations.Migration12 import io.github.wulkanowy.data.db.migrations.Migration12
@ -87,6 +89,7 @@ import io.github.wulkanowy.data.db.migrations.Migration33
import io.github.wulkanowy.data.db.migrations.Migration34 import io.github.wulkanowy.data.db.migrations.Migration34
import io.github.wulkanowy.data.db.migrations.Migration35 import io.github.wulkanowy.data.db.migrations.Migration35
import io.github.wulkanowy.data.db.migrations.Migration36 import io.github.wulkanowy.data.db.migrations.Migration36
import io.github.wulkanowy.data.db.migrations.Migration37
import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration6
@ -125,6 +128,7 @@ import javax.inject.Singleton
Conference::class, Conference::class,
TimetableAdditional::class, TimetableAdditional::class,
StudentInfo::class, StudentInfo::class,
TimetableHeader::class,
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -133,7 +137,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 36 const val VERSION_SCHEMA = 37
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -170,7 +174,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration33(), Migration33(),
Migration34(), Migration34(),
Migration35(appInfo), Migration35(appInfo),
Migration36() Migration36(),
Migration37(),
) )
fun newInstance( fun newInstance(
@ -236,4 +241,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val timetableAdditionalDao: TimetableAdditionalDao abstract val timetableAdditionalDao: TimetableAdditionalDao
abstract val studentInfoDao: StudentInfoDao abstract val studentInfoDao: StudentInfoDao
abstract val timetableHeaderDao: TimetableHeaderDao
} }

View File

@ -0,0 +1,16 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.TimetableHeader
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import javax.inject.Singleton
@Dao
@Singleton
interface TimetableHeaderDao : BaseDao<TimetableHeader> {
@Query("SELECT * FROM TimetableHeaders WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<TimetableHeader>>
}

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
import java.time.LocalDate
@Entity(tableName = "TimetableHeaders")
data class TimetableHeader(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "diary_id")
val diaryId: Int,
val date: LocalDate,
val content: String,
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,21 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration37 : Migration(36, 37) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS TimetableHeaders (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
student_id INTEGER NOT NULL,
diary_id INTEGER NOT NULL,
date INTEGER NOT NULL,
content TEXT NOT NULL
)
"""
)
}
}

View File

@ -3,9 +3,19 @@ package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.sdk.pojo.TimetableFull as SdkTimetableFull
import io.github.wulkanowy.sdk.pojo.TimetableDayHeader as SdkTimetableHeader
import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable
import io.github.wulkanowy.sdk.pojo.TimetableAdditional as SdkTimetableAdditional import io.github.wulkanowy.sdk.pojo.TimetableAdditional as SdkTimetableAdditional
fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull(
lessons = lessons.mapToEntities(semester),
additional = additional.mapToEntities(semester),
headers = headers.mapToEntities(semester)
)
fun List<SdkTimetable>.mapToEntities(semester: Semester) = map { fun List<SdkTimetable>.mapToEntities(semester: Semester) = map {
Timetable( Timetable(
studentId = semester.studentId, studentId = semester.studentId,
@ -39,3 +49,13 @@ fun List<SdkTimetableAdditional>.mapToEntities(semester: Semester) = map {
end = it.end end = it.end
) )
} }
@JvmName("mapToEntitiesTimetableHeaders")
fun List<SdkTimetableHeader>.mapToEntities(semester: Semester) = map {
TimetableHeader(
studentId = semester.studentId,
diaryId = semester.diaryId,
date = it.date,
content = it.content
)
}

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.db.entities.TimetableHeader
data class TimetableFull(
val lessons: List<Timetable>,
val additional: List<TimetableAdditional>,
val headers: List<TimetableHeader>,
)

View File

@ -2,11 +2,14 @@ package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
@ -16,8 +19,8 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -27,6 +30,7 @@ import javax.inject.Singleton
class TimetableRepository @Inject constructor( class TimetableRepository @Inject constructor(
private val timetableDb: TimetableDao, private val timetableDb: TimetableDao,
private val timetableAdditionalDb: TimetableAdditionalDao, private val timetableAdditionalDb: TimetableAdditionalDao,
private val timetableHeaderDb: TimetableHeaderDao,
private val sdk: Sdk, private val sdk: Sdk,
private val schedulerHelper: TimetableNotificationSchedulerHelper, private val schedulerHelper: TimetableNotificationSchedulerHelper,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
@ -36,53 +40,111 @@ class TimetableRepository @Inject constructor(
private val cacheKey = "timetable" private val cacheKey = "timetable"
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource( fun getTimetable(
student: Student, semester: Semester, start: LocalDate, end: LocalDate,
forceRefresh: Boolean, refreshAdditional: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = { (timetable, additional, headers) ->
query = { val refreshKey = getRefreshKey(cacheKey, semester, start, end)
timetableDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) val isShouldRefresh = refreshHelper.isShouldBeRefreshed(refreshKey)
.map { schedulerHelper.scheduleNotifications(it, student); it } val isRefreshAdditional = additional.isEmpty() && refreshAdditional
.combine(timetableAdditionalDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)) { timetable, additional ->
timetable to additional
}
},
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getTimetable(start.monday, end.sunday)
.let { (normal, additional) -> normal.mapToEntities(semester) to additional.mapToEntities(semester) }
val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty()
isNoData || forceRefresh || isShouldRefresh
}, },
saveFetchResult = { (oldTimetable, oldAdditional), (newTimetable, newAdditional) -> query = { getFullTimetableFromDatabase(student, semester, start, end) },
refreshTimetable(student, oldTimetable, newTimetable) fetch = {
refreshAdditional(oldAdditional, newAdditional) val timetableFull = sdk.init(student)
.switchDiary(semester.diaryId, semester.schoolYear)
.getTimetableFull(start.monday, end.sunday)
timetableFull.mapToEntities(semester)
},
saveFetchResult = { timetableOld, timetableNew ->
refreshTimetable(student, timetableOld.lessons, timetableNew.lessons)
refreshAdditional(timetableOld.additional, timetableNew.additional)
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}, },
filterResult = { (timetable, additional) -> filterResult = { (timetable, additional, headers) ->
timetable.filter { item -> TimetableFull(
item.date in start..end lessons = timetable.filter { it.date in start..end },
} to additional.filter { item -> additional = additional.filter { it.date in start..end },
item.date in start..end headers = headers.filter { it.date in start..end }
} )
} }
) )
private suspend fun refreshTimetable(student: Student, old: List<Timetable>, new: List<Timetable>) { private fun getFullTimetableFromDatabase(
timetableDb.deleteAll(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) }) student: Student, semester: Semester,
timetableDb.insertAll(new.uniqueSubtract(old).also { schedulerHelper.scheduleNotifications(it, student) }.map { item -> start: LocalDate, end: LocalDate
item.also { new -> ): Flow<TimetableFull> {
old.singleOrNull { new.start == it.start }?.let { old -> val timetableFlow = timetableDb.loadAll(
return@map new.copy( diaryId = semester.diaryId,
room = if (new.room.isEmpty()) old.room else new.room, studentId = semester.studentId,
teacher = if (new.teacher.isEmpty() && !new.changes && !old.changes) old.teacher else new.teacher from = start.monday,
end = end.sunday
)
val headersFlow = timetableHeaderDb.loadAll(
diaryId = semester.diaryId,
studentId = semester.studentId,
from = start.monday,
end = end.sunday
)
val additionalFlow = timetableAdditionalDb.loadAll(
diaryId = semester.diaryId,
studentId = semester.studentId,
from = start.monday,
end = end.sunday
)
return combine(timetableFlow, headersFlow, additionalFlow) { lessons, headers, additional ->
schedulerHelper.scheduleNotifications(lessons, student)
TimetableFull(
lessons = lessons,
headers = headers,
additional = additional
) )
} }
} }
})
private suspend fun refreshTimetable(
student: Student,
lessonsOld: List<Timetable>, lessonsNew: List<Timetable>
) {
val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew
val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new ->
val matchingOld = lessonsOld.singleOrNull { new.start == it.start }
if (matchingOld != null) {
val useOldTeacher = new.teacher.isEmpty() && !new.changes && !matchingOld.changes
new.copy(
room = if (new.room.isEmpty()) matchingOld.room else new.room,
teacher = if (useOldTeacher) matchingOld.teacher
else new.teacher
)
} else new
} }
private suspend fun refreshAdditional(old: List<TimetableAdditional>, new: List<TimetableAdditional>) { timetableDb.deleteAll(lessonsToRemove)
timetableAdditionalDb.deleteAll(old.uniqueSubtract(new)) timetableDb.insertAll(lessonsToAdd)
timetableAdditionalDb.insertAll(new.uniqueSubtract(old))
schedulerHelper.cancelScheduled(lessonsToRemove, student)
schedulerHelper.scheduleNotifications(lessonsToAdd, student)
}
private suspend fun refreshAdditional(
old: List<TimetableAdditional>,
new: List<TimetableAdditional>
) {
timetableAdditionalDb.deleteAll(old uniqueSubtract new)
timetableAdditionalDb.insertAll(new uniqueSubtract old)
}
private suspend fun refreshDayHeaders(old: List<TimetableHeader>, new: List<TimetableHeader>) {
timetableHeaderDb.deleteAll(old uniqueSubtract new)
timetableHeaderDb.insertAll(new uniqueSubtract old)
} }
} }

View File

@ -51,7 +51,8 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
lesson: Timetable lesson: Timetable
) = day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30) ) = day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30)
suspend fun cancelScheduled(lessons: List<Timetable>, studentId: Int = 1) { suspend fun cancelScheduled(lessons: List<Timetable>, student: Student) {
val studentId = student.studentId
withContext(dispatchersProvider.backgroundThread) { withContext(dispatchersProvider.backgroundThread) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
@ -78,7 +79,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) { suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) { if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) {
return cancelScheduled(lessons, student.studentId) return cancelScheduled(lessons, student)
} }
withContext(dispatchersProvider.backgroundThread) { withContext(dispatchersProvider.backgroundThread) {
@ -89,7 +90,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
val canceled = day.filter { it.canceled } val canceled = day.filter { it.canceled }
val active = day.filter { !it.canceled } val active = day.filter { !it.canceled }
cancelScheduled(canceled) cancelScheduled(canceled, student)
active.forEachIndexed { index, lesson -> active.forEachIndexed { index, lesson ->
val intent = createIntent(student, lesson, active.getOrNull(index + 1)) val intent = createIntent(student, lesson, active.getOrNull(index + 1))

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.core.text.HtmlCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -71,7 +72,9 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
with(binding) { with(binding) {
timetableSwipe.setOnRefreshListener(presenter::onSwipeRefresh) timetableSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
timetableSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) timetableSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
timetableSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) timetableSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)
)
timetableErrorRetry.setOnClickListener { presenter.onRetry() } timetableErrorRetry.setOnClickListener { presenter.onRetry() }
timetableErrorDetails.setOnClickListener { presenter.onDetailsClick() } timetableErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -95,7 +98,12 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
} }
} }
override fun updateData(data: List<Timetable>, showWholeClassPlanType: String, showGroupsInPlanType: Boolean, showTimetableTimers: Boolean) { override fun updateData(
data: List<Timetable>,
showWholeClassPlanType: String,
showGroupsInPlanType: Boolean,
showTimetableTimers: Boolean
) {
with(timetableAdapter) { with(timetableAdapter) {
items = data.toMutableList() items = data.toMutableList()
showTimers = showTimetableTimers showTimers = showTimetableTimers
@ -136,6 +144,13 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
binding.timetableEmpty.visibility = if (show) VISIBLE else GONE binding.timetableEmpty.visibility = if (show) VISIBLE else GONE
} }
override fun setDayHeaderMessage(message: String?) {
binding.timetableEmptyMessage.visibility = if (message.isNullOrEmpty()) GONE else VISIBLE
binding.timetableEmptyMessage.text = HtmlCompat.fromHtml(
message.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT
)
}
override fun showErrorView(show: Boolean) { override fun showErrorView(show: Boolean) {
binding.timetableError.visibility = if (show) VISIBLE else GONE binding.timetableError.visibility = if (show) VISIBLE else GONE
} }
@ -172,8 +187,10 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth -> val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
presenter.onDateSet(year, month + 1, dayOfMonth) presenter.onDateSet(year, month + 1, dayOfMonth)
} }
val datePickerDialog = DatePickerDialog.newInstance(dateSetListener, val datePickerDialog = DatePickerDialog.newInstance(
currentDate.year, currentDate.monthValue - 1, currentDate.dayOfMonth) dateSetListener,
currentDate.year, currentDate.monthValue - 1, currentDate.dayOfMonth
)
with(datePickerDialog) { with(datePickerDialog) {
setDateRangeLimiter(SchooldaysRangeLimiter()) setDateRangeLimiter(SchooldaysRangeLimiter())

View File

@ -138,32 +138,37 @@ class TimetablePresenter @Inject constructor(
flowWithResourceIn { flowWithResourceIn {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
timetableRepository.getTimetable(student, semester, currentDate, currentDate, forceRefresh) timetableRepository.getTimetable(
student, semester, currentDate, currentDate, forceRefresh
)
}.onEach { }.onEach {
when (it.status) { when (it.status) {
Status.LOADING -> { Status.LOADING -> {
if (!it.data?.first.isNullOrEmpty()) { if (!it.data?.lessons.isNullOrEmpty()) {
view?.run { view?.run {
enableSwipe(true) enableSwipe(true)
showRefresh(true) showRefresh(true)
showProgress(false) showProgress(false)
showContent(true) showContent(true)
updateData(it.data!!.first) updateData(it.data!!.lessons)
} }
} }
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading timetable result: Success") Timber.i("Loading timetable result: Success")
view?.apply { view?.apply {
updateData(it.data!!.first) updateData(it.data!!.lessons)
showEmpty(it.data.first.isEmpty()) showEmpty(it.data.lessons.isEmpty())
setDayHeaderMessage(it.data.headers.singleOrNull { header ->
header.date == currentDate
}?.content)
showErrorView(false) showErrorView(false)
showContent(it.data.first.isNotEmpty()) showContent(it.data.lessons.isNotEmpty())
} }
analytics.logEvent( analytics.logEvent(
"load_data", "load_data",
"type" to "timetable", "type" to "timetable",
"items" to it.data!!.first.size "items" to it.data!!.lessons.size
) )
} }
Status.ERROR -> { Status.ERROR -> {

View File

@ -24,6 +24,8 @@ interface TimetableView : BaseView {
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)
fun setDayHeaderMessage(message: String?)
fun showErrorView(show: Boolean) fun showErrorView(show: Boolean)
fun setErrorDetails(message: String) fun setErrorDetails(message: String)

View File

@ -110,15 +110,15 @@ class AdditionalLessonsPresenter @Inject constructor(
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading additional lessons lessons result: Success") Timber.i("Loading additional lessons lessons result: Success")
view?.apply { view?.apply {
updateData(it.data!!.second.sortedBy { item -> item.date }) updateData(it.data!!.additional.sortedBy { item -> item.date })
showEmpty(it.data.second.isEmpty()) showEmpty(it.data.additional.isEmpty())
showErrorView(false) showErrorView(false)
showContent(it.data.second.isNotEmpty()) showContent(it.data.additional.isNotEmpty())
} }
analytics.logEvent( analytics.logEvent(
"load_data", "load_data",
"type" to "additional_lessons", "type" to "additional_lessons",
"items" to it.data!!.second.size "items" to it.data!!.additional.size
) )
} }
Status.ERROR -> { Status.ERROR -> {

View File

@ -107,7 +107,7 @@ class TimetableWidgetFactory(
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
timetableRepository.getTimetable(student, semester, date, date, false) timetableRepository.getTimetable(student, semester, date, date, false)
.toFirstResult().data?.first.orEmpty() .toFirstResult().data?.lessons.orEmpty()
.sortedWith(compareBy({ it.number }, { !it.isStudentPlan })) .sortedWith(compareBy({ it.number }, { !it.isStudentPlan }))
.filter { if (prefRepository.showWholeClassPlan == "no") it.isStudentPlan else true } .filter { if (prefRepository.showWholeClassPlan == "no") it.isStudentPlan else true }
} }

View File

@ -15,7 +15,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:indeterminate="true" /> android:indeterminate="true"
tools:visibility="gone" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/timetableSwipe" android:id="@+id/timetableSwipe"
@ -26,7 +27,8 @@
android:id="@+id/timetableRecycler" android:id="@+id/timetableRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:listitem="@layout/item_timetable" /> tools:listitem="@layout/item_timetable"
tools:visibility="visible" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout <LinearLayout
@ -53,6 +55,18 @@
android:gravity="center" android:gravity="center"
android:text="@string/timetable_no_items" android:text="@string/timetable_no_items"
android:textSize="20sp" /> android:textSize="20sp" />
<TextView
android:id="@+id/timetableEmptyMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
android:lineSpacingMultiplier="1.2"
android:textColor="?android:textColorSecondary"
android:textSize="16sp"
tools:maxLines="4"
tools:text="@tools:sample/lorem/random" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View File

@ -121,7 +121,7 @@
app:layout_constraintStart_toEndOf="@+id/timetableItemTeacher" app:layout_constraintStart_toEndOf="@+id/timetableItemTeacher"
app:layout_constraintTop_toTopOf="@+id/timetableItemTimeFinish" app:layout_constraintTop_toTopOf="@+id/timetableItemTimeFinish"
tools:text="Lekcja odwołana: uczniowie zwolnieni do domu" tools:text="Lekcja odwołana: uczniowie zwolnieni do domu"
tools:visibility="visible" /> tools:visibility="gone" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/timetableItemTimeBarrier" android:id="@+id/timetableItemTimeBarrier"

View File

@ -2,10 +2,12 @@ package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getSemesterEntity
import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.TimetableFull
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.toFirstResult import io.github.wulkanowy.utils.toFirstResult
@ -41,6 +43,9 @@ class TimetableRepositoryTest {
@MockK @MockK
private lateinit var timetableAdditionalDao: TimetableAdditionalDao private lateinit var timetableAdditionalDao: TimetableAdditionalDao
@MockK
private lateinit var timetableHeaderDao: TimetableHeaderDao
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
private lateinit var refreshHelper: AutoRefreshHelper private lateinit var refreshHelper: AutoRefreshHelper
@ -59,7 +64,7 @@ class TimetableRepositoryTest {
MockKAnnotations.init(this) MockKAnnotations.init(this)
every { refreshHelper.isShouldBeRefreshed(any()) } returns false every { refreshHelper.isShouldBeRefreshed(any()) } returns false
timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, sdk, timetableNotificationSchedulerHelper, refreshHelper) timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, timetableHeaderDao, sdk, timetableNotificationSchedulerHelper, refreshHelper)
} }
@Test @Test
@ -71,7 +76,7 @@ class TimetableRepositoryTest {
createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "", "W-F"), createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "", "W-F"),
createTimetableRemote(of(2021, 1, 4, 10, 30), 4, "", "W-F") createTimetableRemote(of(2021, 1, 4, 10, 30), 4, "", "W-F")
) )
coEvery { sdk.getTimetable(any(), any()) } returns (remoteList to emptyList()) coEvery { sdk.getTimetableFull(any(), any()) } returns TimetableFull(emptyList(), remoteList, emptyList())
val localList = listOf( val localList = listOf(
createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Przyroda"), createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Przyroda"),
@ -87,13 +92,17 @@ class TimetableRepositoryTest {
coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3)
coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs
coEvery { timetableHeaderDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf())
coEvery { timetableHeaderDao.insertAll(emptyList()) } returns listOf(1, 2, 3)
coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs
// execute // execute
val res = runBlocking { val res = runBlocking {
timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult()
} }
// verify // verify
assertEquals(4, res.data?.first.orEmpty().size) assertEquals(4, res.data?.lessons.orEmpty().size)
coVerify { coVerify {
timetableDb.insertAll(withArg { timetableDb.insertAll(withArg {
assertEquals(4, it.size) assertEquals(4, it.size)
@ -124,7 +133,7 @@ class TimetableRepositoryTest {
createTimetableRemote(of(2021, 1, 6, 9, 40), 3, "125", "Matematyka", "Paweł Środowski", false), createTimetableRemote(of(2021, 1, 6, 9, 40), 3, "125", "Matematyka", "Paweł Środowski", false),
createTimetableRemote(of(2021, 1, 6, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true) createTimetableRemote(of(2021, 1, 6, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true)
) )
coEvery { sdk.getTimetable(startDate, endDate) } returns (remoteList to emptyList()) coEvery { sdk.getTimetableFull(startDate, endDate) } returns TimetableFull(emptyList(), remoteList, emptyList())
val localList = listOf( val localList = listOf(
createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Matematyka", "Paweł Poniedziałkowski", false), createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Matematyka", "Paweł Poniedziałkowski", false),
@ -150,12 +159,16 @@ class TimetableRepositoryTest {
coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3)
coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs
coEvery { timetableHeaderDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf())
coEvery { timetableHeaderDao.insertAll(emptyList()) } returns listOf(1, 2, 3)
coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs
// execute // execute
val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() } val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() }
// verify // verify
assertEquals(null, res.error) assertEquals(null, res.error)
assertEquals(12, res.data!!.first.size) assertEquals(12, res.data!!.lessons.size)
coVerify { coVerify {
timetableDb.insertAll(withArg { timetableDb.insertAll(withArg {
@ -187,7 +200,7 @@ class TimetableRepositoryTest {
) )
// prepare // prepare
coEvery { sdk.getTimetable(startDate, endDate) } returns (remoteList to emptyList()) coEvery { sdk.getTimetableFull(startDate, endDate) } returns TimetableFull(emptyList(), remoteList, emptyList())
coEvery { timetableDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( coEvery { timetableDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)),
flowOf(remoteList.mapToEntities(semester)) flowOf(remoteList.mapToEntities(semester))
@ -199,13 +212,17 @@ class TimetableRepositoryTest {
coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs
coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3)
coEvery { timetableHeaderDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf())
coEvery { timetableHeaderDao.insertAll(emptyList()) } returns listOf(1, 2, 3)
coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs
// execute // execute
val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() } val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() }
// verify // verify
assertEquals(null, res.error) assertEquals(null, res.error)
assertEquals(2, res.data?.first?.size) assertEquals(2, res.data?.lessons?.size)
coVerify { sdk.getTimetable(startDate, endDate) } coVerify { sdk.getTimetableFull(startDate, endDate) }
coVerify { timetableDb.loadAll(1, 1, startDate, endDate) } coVerify { timetableDb.loadAll(1, 1, startDate, endDate) }
coVerify { timetableDb.insertAll(match { it.isEmpty() }) } coVerify { timetableDb.insertAll(match { it.isEmpty() }) }
coVerify { timetableDb.deleteAll(match { it.isEmpty() }) } coVerify { timetableDb.deleteAll(match { it.isEmpty() }) }