diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index 9a6528f3..ffccb059 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -27,9 +28,12 @@ class AttendanceRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "attendance" fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index 4edb507b..cd4403c7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -20,9 +21,12 @@ class AttendanceSummaryRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "attendance_summary" fun getAttendanceSummary(student: Student, semester: Semester, subjectId: Int, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt index 59aabdd5..99ef56f4 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt @@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -23,9 +24,12 @@ class CompletedLessonsRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "completed" fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index befcf9e6..0a839d27 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -20,9 +21,12 @@ class ConferenceRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "conference" fun getConferences(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, query = { conferenceDb.loadAll(semester.diaryId, student.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index bd6e7d2d..a8912f10 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.startExamsDay import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -23,9 +24,12 @@ class ExamRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "exam" fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { examDb.loadAll(semester.diaryId, semester.studentId, start.startExamsDay, start.endExamsDay) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index bab290f3..9880e464 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -16,6 +16,7 @@ import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import java.time.LocalDateTime import javax.inject.Inject import javax.inject.Singleton @@ -28,14 +29,20 @@ class GradeRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "grade" fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( - shouldFetch = { (details, summaries) -> details.isEmpty() || summaries.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, + mutex = saveFetchResultMutex, + shouldFetch = { (details, summaries) -> + val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed + }, query = { - gradeDb.loadAll(semester.semesterId, semester.studentId).combine(gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)) { details, summaries -> - details to summaries - } + val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) + val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) + detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries } }, fetch = { val (details, summary) = sdk.init(student) @@ -92,19 +99,27 @@ class GradeRepository @Inject constructor( } fun getUnreadGrades(semester: Semester): Flow> { - return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isRead } } + return gradeDb.loadAll(semester.semesterId, semester.studentId).map { + it.filter { grade -> !grade.isRead } + } } fun getNotNotifiedGrades(semester: Semester): Flow> { - return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isNotified } } + return gradeDb.loadAll(semester.semesterId, semester.studentId).map { + it.filter { grade -> !grade.isNotified } + } } fun getNotNotifiedPredictedGrades(semester: Semester): Flow> { - return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } } + return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { + it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } + } } fun getNotNotifiedFinalGrades(semester: Semester): Flow> { - return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } } + return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { + it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } + } } suspend fun updateGrade(grade: Grade) { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt index ab65fb14..9cd8e711 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt @@ -17,6 +17,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -30,11 +31,16 @@ class GradeStatisticsRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val partialMutex = Mutex() + private val semesterMutex = Mutex() + private val pointsMutex = Mutex() + private val partialCacheKey = "grade_stats_partial" private val semesterCacheKey = "grade_stats_semester" private val pointsCacheKey = "grade_stats_points" fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + mutex = partialMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { @@ -71,6 +77,7 @@ class GradeStatisticsRepository @Inject constructor( ) fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + mutex = semesterMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { @@ -112,6 +119,7 @@ class GradeStatisticsRepository @Inject constructor( ) fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + mutex = pointsMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index 7625dbbc..068fd9a5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -24,9 +25,12 @@ class HomeworkRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "homework" fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 4f2dcc54..b904b7db 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import java.time.LocalDate.now import javax.inject.Inject @@ -20,7 +21,10 @@ class LuckyNumberRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { luckyNumberDb.load(student.studentId, now()) }, fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index ea7b2b0e..5f555418 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -20,6 +20,7 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import timber.log.Timber import java.time.LocalDateTime.now import javax.inject.Inject @@ -33,10 +34,13 @@ class MessageRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "message" @Suppress("UNUSED_PARAMETER") fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt index 7e83ef7d..4b333bc6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -23,9 +24,12 @@ class MobileDeviceRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "devices" fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) }, query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index 85789f09..85339dfa 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -23,9 +24,12 @@ class NoteRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "note" fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, query = { noteDb.loadAll(student.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt index 6b22b32c..8b59cb58 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt @@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -16,8 +17,11 @@ class SchoolRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { schoolDb.load(semester.studentId, semester.classId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt index e3deb447..de66ad20 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt @@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -16,8 +17,11 @@ class StudentInfoRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { studentInfoDao.loadStudentInfo(student.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt index ef07a1d4..b4bfef18 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -17,7 +18,10 @@ class SubjectRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 25da718c..7135edbe 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -17,7 +18,10 @@ class TeacherRepository @Inject constructor( private val sdk: Sdk ) { + private val saveFetchResultMutex = Mutex() + fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { it.isEmpty() || forceRefresh }, query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index fa1898f5..927565b5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -18,6 +18,7 @@ import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton @@ -31,9 +32,12 @@ class TimetableRepository @Inject constructor( private val refreshHelper: AutoRefreshHelper, ) { + private val saveFetchResultMutex = Mutex() + private val cacheKey = "timetable" fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource( + mutex = saveFetchResultMutex, shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, query = { timetableDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/student/StudentRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/student/StudentRemote.kt deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt index 049e1d42..5dd28967 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/FlowUtils.kt @@ -13,8 +13,11 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock inline fun networkBoundResource( + mutex: Mutex = Mutex(), showSavedOnLoading: Boolean = true, crossinline query: () -> Flow, crossinline fetch: suspend (ResultType) -> RequestType, @@ -31,7 +34,7 @@ inline fun networkBoundResource( try { val newData = fetch(data) - saveFetchResult(data, newData) + mutex.withLock { saveFetchResult(query().first(), newData) } query().map { Resource.success(filterResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) @@ -44,11 +47,12 @@ inline fun networkBoundResource( @JvmName("networkBoundResourceWithMap") inline fun networkBoundResource( + mutex: Mutex = Mutex(), showSavedOnLoading: Boolean = true, crossinline query: () -> Flow, crossinline fetch: suspend (ResultType) -> RequestType, crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline onFetchFailed: (Throwable) -> Unit = { Unit }, + crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline shouldFetch: (ResultType) -> Boolean = { true }, crossinline mapResult: (ResultType) -> T ) = flow { @@ -59,7 +63,8 @@ inline fun networkBoundResource( if (showSavedOnLoading) emit(Resource.loading(mapResult(data))) try { - saveFetchResult(data, fetch(data)) + val newData = fetch(data) + mutex.withLock { saveFetchResult(query().first(), newData) } query().map { Resource.success(mapResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index 4c6a1172..1c592c09 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -87,6 +87,7 @@ class AttendanceRepositoryTest { coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester)), + flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)) ) coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) @@ -114,6 +115,7 @@ class AttendanceRepositoryTest { coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList.dropLast(1) coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), + flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.dropLast(1).mapToEntities(semester)) ) coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt index 461e1809..b116a623 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt @@ -87,6 +87,7 @@ class CompletedLessonsRepositoryTest { coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester)), + flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)) ) coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3) @@ -114,6 +115,7 @@ class CompletedLessonsRepositoryTest { coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList.dropLast(1) coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), + flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.dropLast(1).mapToEntities(semester)) ) coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt index 42a89707..ead6dc5d 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt @@ -88,6 +88,7 @@ class ExamRemoteTest { coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester)), + flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)) ) coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3) @@ -115,6 +116,7 @@ class ExamRemoteTest { coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList.dropLast(1) coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), + flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.dropLast(1).mapToEntities(semester)) ) coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 002c7ad7..8a19d633 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -57,7 +57,7 @@ class GradeRepositoryTest { coEvery { gradeDb.deleteAll(any()) } just Runs coEvery { gradeDb.insertAll(any()) } returns listOf() - coEvery { gradeSummaryDb.loadAll(1, 1) } returnsMany listOf(flowOf(listOf()), flowOf(listOf())) + coEvery { gradeSummaryDb.loadAll(1, 1) } returnsMany listOf(flowOf(listOf()), flowOf(listOf()), flowOf(listOf())) coEvery { gradeSummaryDb.deleteAll(any()) } just Runs coEvery { gradeSummaryDb.insertAll(any()) } returns listOf() } @@ -76,7 +76,8 @@ class GradeRepositoryTest { coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), // empty because it is new user - flowOf(remoteList.mapToEntities(semester)) + flowOf(listOf()), // empty again, after fetch end before save result + flowOf(remoteList.mapToEntities(semester)), ) // execute @@ -114,6 +115,7 @@ class GradeRepositoryTest { ) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(localList.mapToEntities(semester)), + flowOf(localList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)) ) @@ -155,6 +157,7 @@ class GradeRepositoryTest { ) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(localList.mapToEntities(semester)), + flowOf(localList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)) ) @@ -184,6 +187,7 @@ class GradeRepositoryTest { ) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(localList.mapToEntities(semester)), + flowOf(localList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)) ) @@ -209,6 +213,7 @@ class GradeRepositoryTest { coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), + flowOf(listOf()), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)) ) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt index 9cbad8ac..a89aad35 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt @@ -72,6 +72,7 @@ class LuckyNumberRemoteTest { coEvery { sdk.getLuckyNumber(student.schoolShortName) } returns luckyNumber coEvery { luckyNumberDb.load(1, date) } returnsMany listOf( flowOf(luckyNumber.mapToEntity(student).copy(luckyNumber = 6666)), + flowOf(luckyNumber.mapToEntity(student).copy(luckyNumber = 6666)), // after fetch end before save result flowOf(luckyNumber.mapToEntity(student)) ) coEvery { luckyNumberDb.insertAll(any()) } returns listOf(1, 2, 3) @@ -101,6 +102,7 @@ class LuckyNumberRemoteTest { coEvery { sdk.getLuckyNumber(student.schoolShortName) } returns luckyNumber coEvery { luckyNumberDb.load(1, date) } returnsMany listOf( flowOf(null), + flowOf(null), // after fetch end before save result flowOf(luckyNumber.mapToEntity(student)) ) coEvery { luckyNumberDb.insertAll(any()) } returns listOf(1, 2, 3) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt index 4a4f2c76..e5b3d101 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt @@ -82,6 +82,7 @@ class MobileDeviceRepositoryTest { coEvery { sdk.getRegisteredDevices() } returns remoteList coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester)), + flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)) ) coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3) @@ -109,6 +110,7 @@ class MobileDeviceRepositoryTest { coEvery { sdk.getRegisteredDevices() } returns remoteList.dropLast(1) coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), + flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.dropLast(1).mapToEntities(semester)) ) coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3) diff --git a/app/src/test/java/io/github/wulkanowy/utils/FlowUtilsKtTest.kt b/app/src/test/java/io/github/wulkanowy/utils/FlowUtilsKtTest.kt new file mode 100644 index 00000000..375a2403 --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/utils/FlowUtilsKtTest.kt @@ -0,0 +1,192 @@ +package io.github.wulkanowy.utils + +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.just +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class FlowUtilsKtTest { + + private val testScope = TestCoroutineScope() + + @Test + fun `fetch from two places with same remote data`() { + val repo = mockk() + coEvery { repo.query() } returnsMany listOf( + // initial data + flowOf(listOf(1, 2, 3)), + flowOf(listOf(1, 2, 3)), + + // for first + flowOf(listOf(1, 2, 3)), // before save + flowOf(listOf(2, 3, 4)), // after save + + // for second + flowOf(listOf(2, 3, 4)), // before save + flowOf(listOf(2, 3, 4)), // after save + ) + coEvery { repo.fetch() } returnsMany listOf( + listOf(2, 3, 4), + listOf(2, 3, 4), + ) + coEvery { repo.save(any(), any()) } just Runs + + // first + networkBoundResource( + showSavedOnLoading = false, + query = { repo.query() }, + fetch = { + val data = repo.fetch() + delay(2_000) + data + }, + saveFetchResult = { old, new -> repo.save(old, new) } + ).launchIn(testScope) + + testScope.advanceTimeBy(1_000) + + // second + networkBoundResource( + showSavedOnLoading = false, + query = { repo.query() }, + fetch = { + val data = repo.fetch() + delay(2_000) + data + }, + saveFetchResult = { old, new -> repo.save(old, new) } + ).launchIn(testScope) + + testScope.advanceTimeBy(3_000) + + coVerifyOrder { + // from first + repo.query() + repo.fetch() // hang for 2 sec + + // wait 1 sec + + // from second + repo.query() + repo.fetch() // hang for 2 sec + + // from first + repo.query() + repo.save(withArg { + assertEquals(listOf(1, 2, 3), it) + }, any()) + repo.query() + + // from second + repo.query() + repo.save(withArg { + assertEquals(listOf(2, 3, 4), it) + }, any()) + repo.query() + } + } + + @Test + fun `fetch from two places with same remote data and save at the same moment`() { + val repo = mockk() + coEvery { repo.query() } returnsMany listOf( + // initial data + flowOf(listOf(1, 2, 3)), + flowOf(listOf(1, 2, 3)), + + // for first + flowOf(listOf(1, 2, 3)), // before save + flowOf(listOf(2, 3, 4)), // after save + + // for second + flowOf(listOf(2, 3, 4)), // before save + flowOf(listOf(2, 3, 4)), // after save + ) + coEvery { repo.fetch() } returnsMany listOf( + listOf(2, 3, 4), + listOf(2, 3, 4), + ) + coEvery { repo.save(any(), any()) } just Runs + + val saveResultMutex = Mutex() + + // first + networkBoundResource( + mutex = saveResultMutex, + showSavedOnLoading = false, + query = { repo.query() }, + fetch = { + val data = repo.fetch() + delay(2_000) + data + }, + saveFetchResult = { old, new -> + delay(1_500) + repo.save(old, new) + } + ).launchIn(testScope) + + testScope.advanceTimeBy(1_000) + + // second + networkBoundResource( + mutex = saveResultMutex, + showSavedOnLoading = false, + query = { repo.query() }, + fetch = { + val data = repo.fetch() + delay(2_000) + data + }, + saveFetchResult = { old, new -> + repo.save(old, new) + } + ).launchIn(testScope) + + testScope.advanceTimeBy(3_000) + + coVerifyOrder { + // from first + repo.query() + repo.fetch() // hang for 2 sec + + // wait 1 sec + + // from second + repo.query() + repo.fetch() // hang for 2 sec + + // from first + repo.query() + repo.save(withArg { + assertEquals(listOf(1, 2, 3), it) + }, any()) + + // from second + repo.query() + repo.save(withArg { + assertEquals(listOf(2, 3, 4), it) + }, any()) + + repo.query() + repo.query() + } + } + + @Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier") + private class TestRepo { + fun query() = flowOf>() + suspend fun fetch() = listOf() + suspend fun save(old: List, new: List) {} + } +}