1
0

Compare commits

...

44 Commits

Author SHA1 Message Date
3e8e9b4ecc Merge branch 'release/0.20.5' into master 2020-09-19 13:03:28 +02:00
d6ebc343d5 Version 0.20.5 2020-09-19 01:25:34 +02:00
73be416807 Fix crash in flowWithResourceIn() (#967) 2020-09-19 00:57:55 +02:00
0cb65a29ba Merge tag '0.20.4' into develop
Version 0.20.4
2020-09-13 19:00:45 +02:00
13198f2ab4 Merge branch 'release/0.20.4' into master 2020-09-13 19:00:39 +02:00
cd92f37435 Version 0.20.4 2020-09-13 19:00:32 +02:00
5d8fb376ab Expand exam sync date range to next month (#960) 2020-09-13 18:37:34 +02:00
47150364d8 Fix lifecycle of timer tasks in timetable lessons (#958)
Co-authored-by: Faierbel <RafalBO99@outlook.com>
2020-09-13 16:27:53 +02:00
792b123598 Bump coil from 1.0.0-rc1 to 1.0.0-rc2 (#961) 2020-09-13 14:07:30 +00:00
acf5c8e9ba Bump firebase-crashlytics-gradle from 2.2.1 to 2.3.0 (#964) 2020-09-13 14:04:41 +00:00
53561668fc Bump hilt_version from 2.28.3-alpha to 2.29.1-alpha (#962) 2020-09-13 14:03:09 +00:00
7cfe58d311 Bump material from 1.2.0 to 1.2.1 (#963) 2020-09-13 14:02:31 +00:00
cd51fac621 Add eduportal.koszalin.pl register (#959) 2020-09-13 13:46:45 +02:00
adde5541e2 Move timetable notifications scheduling to background thread (#954) 2020-09-11 13:02:16 +02:00
6e56d3ff06 Ignore empty semesters on refresh (#955) 2020-09-09 13:28:44 +02:00
ec761f6329 Fix bug in grade statistics (#951) 2020-09-08 20:13:17 +02:00
6363c90e37 Disable sound of upcoming lessons notification (fix) (#950) 2020-09-07 20:28:32 +02:00
c30f105be5 Fix crash on unknown attendance category type (#949) 2020-09-07 09:35:26 +02:00
9f85b2206a Merge tag '0.20.3' into develop
Version 0.20.3
2020-09-04 23:42:12 +02:00
42515fd084 Merge branch 'release/0.20.3' into master 2020-09-04 23:42:07 +02:00
9a7c04fe7b Version 0.20.3 2020-09-04 23:42:01 +02:00
debb21f5f9 Add full stacktrace to errors list in sync now (#945) 2020-09-03 21:10:39 +02:00
18b9bf42e1 Fix crash in flowWithResourceIn() (#944) 2020-09-03 20:54:28 +02:00
6ded83d132 Fix attendance item description (#943) 2020-09-03 20:52:24 +02:00
71d37a1c6c Merge tag '0.20.2' into develop
Version 0.20.2
2020-09-02 00:23:49 +02:00
3975d06cde Merge branch 'release/0.20.2' into master 2020-09-02 00:23:45 +02:00
ee168bafe0 Version 0.20.2 2020-09-02 00:23:41 +02:00
42ed7e0ae1 Merge tag '0.20.1' into develop
Version 0.20.1
2020-09-02 00:14:35 +02:00
0e92447974 Merge branch 'release/0.20.1' into master 2020-09-02 00:14:23 +02:00
40492e6c01 Version 0.20.1 2020-09-02 00:14:18 +02:00
69a1193154 Fix semester list refresh on no current semester found (#940) 2020-09-01 23:58:18 +02:00
0f65af8958 Fix grade summary empty view (#941) 2020-09-01 23:57:56 +02:00
2ad1d086e0 Fix lucky number notification (#937) 2020-09-01 15:39:34 +02:00
f8b7baef24 Remove force sync dialog (#938) 2020-09-01 14:58:45 +02:00
90be9d1add Disable notification sound (#936) 2020-09-01 12:57:45 +02:00
20f931c5cc Fix recaptcha loading in password recover (#935) 2020-09-01 09:33:14 +02:00
9997b1adbb Add skarzyskokamienna vulcan register (#934) 2020-09-01 09:31:36 +02:00
eb616eedc7 Fix crash in flowWithResourceIn() (#933) 2020-09-01 09:31:01 +02:00
a5de39a366 Update UI dependencies (#927) 2020-08-31 12:55:51 +02:00
57bc2b2533 Bump firebase-crashlytics from 17.1.1 to 17.2.1 (#931) 2020-08-31 10:09:37 +00:00
d1ce16d2b1 Bump runner from 1.2.0 to 1.3.0 (#932) 2020-08-31 10:03:20 +00:00
54fb01cd0d Bump firebase-inappmessaging-display-ktx from 19.1.0 to 19.1.1 (#930) 2020-08-31 09:42:25 +00:00
370cfbf22a Bump core from 1.2.0 to 1.3.0 (#929) 2020-08-31 09:42:18 +00:00
d198a2ba21 Merge tag '0.20.0' into develop
Version 0.20.0
2020-08-29 23:57:20 +02:00
45 changed files with 246 additions and 145 deletions

View File

@ -14,7 +14,7 @@ cache:
branches:
only:
- develop
- 0.20.0
- 0.20.5
android:
licenses:

View File

@ -47,7 +47,6 @@ You can also download a [development version](https://wulkanowy.github.io/#downl
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [RxJava 2](https://github.com/ReactiveX/RxJava)
* [Dagger 2](https://github.com/google/dagger)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)

View File

@ -48,7 +48,6 @@ Możesz także pobrać [wersję rozwojową](https://wulkanowy.github.io/#downloa
## Zbudowana za pomocą
* [Wulkanowy SDK](https://github.com/wulkanowy/SDK)
* [RxJava 2](https://github.com/ReactiveX/RxJava)
* [Dagger 2](https://github.com/google/dagger)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)

View File

@ -18,8 +18,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17
targetSdkVersion 29
versionCode 64
versionName "0.20.0"
versionCode 69
versionName "0.20.5"
multiDexEnabled true
resValue "string", "app_name", "Wulkanowy"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -126,7 +126,7 @@ configurations.all {
}
dependencies {
implementation "io.github.wulkanowy:sdk:0.20.0"
implementation "io.github.wulkanowy:sdk:0.20.5"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
@ -145,9 +145,9 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.constraintlayout:constraintlayout:2.0.1"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.1.0"
implementation "com.google.android.material:material:1.2.1"
implementation "com.github.wulkanowy:material-chips-input:2.1.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
@ -176,15 +176,15 @@ dependencies {
implementation "fr.bipi.treessence:treessence:0.3.2"
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
implementation "io.coil-kt:coil:1.0.0-rc1"
implementation "io.coil-kt:coil:1.0.0-rc2"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
playImplementation 'com.google.firebase:firebase-analytics:17.5.0'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.1.0'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.1.1'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.1.1"
playImplementation 'com.google.firebase:firebase-messaging:20.2.4'
playImplementation 'com.google.firebase:firebase-crashlytics:17.1.1'
playImplementation 'com.google.firebase:firebase-crashlytics:17.2.1'
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -196,8 +196,8 @@ dependencies {
testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9'
androidTestImplementation "androidx.test:core:1.2.0"
androidTestImplementation "androidx.test:runner:1.2.0"
androidTestImplementation "androidx.test:core:1.3.0"
androidTestImplementation "androidx.test:runner:1.3.0"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "androidx.room:room-testing:$room"

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.data
data class Resource<out T>(val status: Status, val data: T?, val error: Throwable?) {
data class Resource<T>(val status: Status, val data: T?, val error: Throwable?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)

View File

@ -12,5 +12,5 @@ import javax.inject.Singleton
interface LuckyNumberDao : BaseDao<LuckyNumber> {
@Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date = :date")
fun load(studentId: Int, date: LocalDate): Flow<LuckyNumber>
fun load(studentId: Int, date: LocalDate): Flow<LuckyNumber?>
}

View File

@ -12,7 +12,7 @@ interface MessagesDao : BaseDao<Message> {
@Transaction
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND message_id = :messageId")
fun loadMessageWithAttachment(studentId: Int, messageId: Int): Flow<MessageWithAttachment>
fun loadMessageWithAttachment(studentId: Int, messageId: Int): Flow<MessageWithAttachment?>
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND folder_id = :folder ORDER BY date DESC")
fun loadAll(studentId: Int, folder: Int): Flow<List<Message>>

View File

@ -36,12 +36,6 @@ data class Message(
var unread: Boolean,
@ColumnInfo(name = "unread_by")
val unreadBy: Int,
@ColumnInfo(name = "read_by")
val readBy: Int,
val removed: Boolean,
@ColumnInfo(name = "has_attachments")
@ -54,5 +48,11 @@ data class Message(
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
@ColumnInfo(name = "unread_by")
var unreadBy: Int = 0
@ColumnInfo(name = "read_by")
var readBy: Int = 0
var content: String = ""
}

View File

@ -2,9 +2,9 @@ package io.github.wulkanowy.data.repositories.exam
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.endExamsDay
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.startExamsDay
import io.github.wulkanowy.utils.uniqueSubtract
import java.time.LocalDate
import javax.inject.Inject
@ -18,8 +18,8 @@ class ExamRepository @Inject constructor(
fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
shouldFetch = { it.isEmpty() || forceRefresh },
query = { local.getExams(semester, start.monday, end.sunday) },
fetch = { remote.getExams(student, semester, start.monday, end.sunday) },
query = { local.getExams(semester, start.startExamsDay, start.endExamsDay) },
fetch = { remote.getExams(student, semester, start.startExamsDay, start.endExamsDay) },
saveFetchResult = { old, new ->
local.deleteExams(old uniqueSubtract new)
local.saveExams(new uniqueSubtract old)

View File

@ -3,7 +3,8 @@ package io.github.wulkanowy.data.repositories.luckynumber
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.time.LocalDate.now
import javax.inject.Inject
import javax.inject.Singleton
@ -28,11 +29,8 @@ class LuckyNumberRepository @Inject constructor(
}
)
fun getNotNotifiedLuckyNumber(student: Student): Flow<LuckyNumber?> {
return local.getLuckyNumber(student, now())
}
suspend fun getNotNotifiedLuckyNumber(student: Student) =
local.getLuckyNumber(student, now()).map { if (it?.isNotified == false) it else null }.first()
suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) {
local.updateLuckyNumber(luckyNumber)
}
suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = local.updateLuckyNumber(luckyNumber)
}

View File

@ -6,7 +6,6 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.message.MessageFolder.TRASHED
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
@ -29,7 +28,7 @@ class MessageLocal @Inject constructor(
messagesDb.deleteAll(messages)
}
fun getMessageWithAttachment(student: Student, message: Message): Flow<MessageWithAttachment> {
fun getMessageWithAttachment(student: Student, message: Message): Flow<MessageWithAttachment?> {
return messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId)
}

View File

@ -30,12 +30,12 @@ class MessageRemote @Inject constructor(private val sdk: Sdk) {
date = it.date ?: now(),
folderId = it.folderId,
unread = it.unread ?: false,
unreadBy = it.unreadBy ?: 0,
readBy = it.readBy ?: 0,
removed = it.removed,
hasAttachments = it.hasAttachments
).apply {
content = it.content.orEmpty()
unreadBy = it.unreadBy ?: 0
readBy = it.readBy ?: 0
}
}
}

View File

@ -34,12 +34,14 @@ class MessageRepository @Inject constructor(
fun getMessage(student: Student, message: Message, markAsRead: Boolean = false) = networkBoundResource(
shouldFetch = {
checkNotNull(it, { "This message no longer exist!" })
Timber.d("Message content in db empty: ${it.message.content.isEmpty()}")
it.message.unread || it.message.content.isEmpty()
},
query = { local.getMessageWithAttachment(student, message) },
fetch = { remote.getMessagesContentDetails(student, it.message, markAsRead) },
fetch = { remote.getMessagesContentDetails(student, it!!.message, markAsRead) },
saveFetchResult = { old, (downloadedMessage, attachments) ->
checkNotNull(old, { "Fetched message no longer exist!" })
local.updateMessages(listOf(old.message.copy(unread = !markAsRead).apply {
id = old.message.id
content = content.ifBlank { downloadedMessage }

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories.semester
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider
@ -7,6 +8,7 @@ import io.github.wulkanowy.utils.getCurrentOrLast
import io.github.wulkanowy.utils.isCurrent
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -18,24 +20,33 @@ class SemesterRepository @Inject constructor(
) {
suspend fun getSemesters(student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false) = withContext(dispatchers.backgroundThread) {
local.getSemesters(student).let { semesters ->
semesters.filter {
!forceRefresh && when {
Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API -> semesters.firstOrNull { it.isCurrent }?.diaryId != 0
refreshOnNoCurrent -> semesters.any { semester -> semester.isCurrent }
else -> true
}
}
}.ifEmpty {
val new = remote.getSemesters(student)
if (new.isEmpty()) throw IllegalArgumentException("Empty semester list!")
val old = local.getSemesters(student)
local.deleteSemesters(old.uniqueSubtract(new))
local.saveSemesters(new.uniqueSubtract(old))
val semesters = local.getSemesters(student)
if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) {
refreshSemesters(student)
local.getSemesters(student)
}
} else semesters
}
private fun isShouldFetch(student: Student, semesters: List<Semester>, forceRefresh: Boolean, refreshOnNoCurrent: Boolean): Boolean {
val isNoSemesters = semesters.isEmpty()
val isRefreshOnModeChangeRequired = if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
semesters.firstOrNull { it.isCurrent }?.diaryId == 0
} else false
val isRefreshOnNoCurrentAppropriate = refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent }
return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate
}
private suspend fun refreshSemesters(student: Student) {
val new = remote.getSemesters(student)
if (new.isEmpty()) return Timber.i("Empty semester list!")
val old = local.getSemesters(student)
local.deleteSemesters(old.uniqueSubtract(new))
local.saveSemesters(new.uniqueSubtract(old))
}
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = withContext(dispatchers.backgroundThread) {

View File

@ -26,7 +26,9 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.time.LocalDateTime
import java.time.LocalDateTime.now
@ -35,7 +37,8 @@ import javax.inject.Inject
class TimetableNotificationSchedulerHelper @Inject constructor(
@ApplicationContext private val context: Context,
private val alarmManager: AlarmManager,
private val preferencesRepository: PreferencesRepository
private val preferencesRepository: PreferencesRepository,
private val dispatchersProvider: DispatchersProvider,
) {
private fun getRequestCode(time: LocalDateTime, studentId: Int) = (time.toTimestamp() * studentId).toInt()
@ -44,13 +47,15 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
return day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30)
}
fun cancelScheduled(lessons: List<Timetable>, studentId: Int = 1) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo(upcomingTime..lesson.start, getRequestCode(upcomingTime, studentId))
cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId))
suspend fun cancelScheduled(lessons: List<Timetable>, studentId: Int = 1) {
withContext(dispatchersProvider.backgroundThread) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo(upcomingTime..lesson.start, getRequestCode(upcomingTime, studentId))
cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId))
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
}
}
}
@ -61,28 +66,30 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
fun cancelNotification() = NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) return cancelScheduled(lessons, student.studentId)
lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } }
.map { it.filter { lesson -> !lesson.canceled && lesson.isStudentPlan } }
.map { day ->
day.forEachIndexed { index, lesson ->
val intent = createIntent(student, lesson, day.getOrNull(index + 1))
withContext(dispatchersProvider.backgroundThread) {
lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } }
.map { it.filter { lesson -> !lesson.canceled && lesson.isStudentPlan } }
.map { day ->
day.forEachIndexed { index, lesson ->
val intent = createIntent(student, lesson, day.getOrNull(index + 1))
if (lesson.start > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_UPCOMING, getUpcomingLessonTime(index, day, lesson))
}
if (lesson.start > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_UPCOMING, getUpcomingLessonTime(index, day, lesson))
}
if (lesson.end > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_CURRENT, lesson.start)
if (day.lastIndex == index) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, lesson.end)
if (lesson.end > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_CURRENT, lesson.start)
if (day.lastIndex == index) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, lesson.end)
}
}
}
}
}
}
}
private fun createIntent(student: Student, lesson: Timetable, nextLesson: Timetable?): Intent {

View File

@ -44,6 +44,7 @@ class SyncManager @Inject constructor(
if (SDK_INT >= O) {
channels.forEach { it.create() }
notificationManager.deleteNotificationChannel("lesson_channel")
notificationManager.deleteNotificationChannel("new_entries_channel")
}

View File

@ -50,13 +50,16 @@ class SyncWorker @WorkerInject constructor(
} catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException) null
else e
else {
Timber.e(e)
e
}
}
}
val result = when {
exceptions.isNotEmpty() && inputData.getBoolean("one_time", false) -> {
Result.failure(Data.Builder()
.putString("error", exceptions.toString())
.putString("error", exceptions.map { it.stackTraceToString() }.toString())
.build()
)
}

View File

@ -17,7 +17,7 @@ class UpcomingLessonsChannel @Inject constructor(
) : Channel {
companion object {
const val CHANNEL_ID = "lesson_channel"
const val CHANNEL_ID = "upcoming_lesson_channel"
}
override fun create() {
@ -26,6 +26,7 @@ class UpcomingLessonsChannel @Inject constructor(
lockscreenVisibility = VISIBILITY_PUBLIC
setShowBadge(false)
enableVibration(false)
setSound(null, null)
}
)
}

View File

@ -3,8 +3,6 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.exam.ExamRepository
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.waitForResult
import java.time.LocalDate.now
import javax.inject.Inject
@ -12,6 +10,6 @@ import javax.inject.Inject
class ExamWork @Inject constructor(private val examRepository: ExamRepository) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
examRepository.getExams(student, semester, now().monday, now().sunday, true).waitForResult()
examRepository.getExams(student, semester, now(), now(), true).waitForResult()
}
}

View File

@ -19,7 +19,6 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import javax.inject.Inject
import kotlin.random.Random
@ -33,7 +32,7 @@ class LuckyNumberWork @Inject constructor(
override suspend fun doWork(student: Student, semester: Semester) {
luckyNumberRepository.getLuckyNumber(student, true, preferencesRepository.isNotificationsEnable).waitForResult()
luckyNumberRepository.getNotNotifiedLuckyNumber(student).first()?.let {
luckyNumberRepository.getNotNotifiedLuckyNumber(student)?.let {
notify(it)
luckyNumberRepository.updateLuckyNumber(it.apply { isNotified = true })
}

View File

@ -7,6 +7,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
@ -63,7 +64,7 @@ open class BasePresenter<T : BaseView>(
fun <T> Flow<T>.launch(individualJobTag: String = "load"): Job {
jobs[individualJobTag]?.cancel()
val job = launchIn(this@BasePresenter)
val job = catch { errorHandler.dispatch(it) }.launchIn(this@BasePresenter)
jobs[individualJobTag] = job
Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job")
return job

View File

@ -9,6 +9,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.repositories.attendance.SentExcuseStatus
import io.github.wulkanowy.databinding.ItemAttendanceBinding
import io.github.wulkanowy.utils.description
import javax.inject.Inject
class AttendanceAdapter @Inject constructor() :
@ -34,7 +35,7 @@ class AttendanceAdapter @Inject constructor() :
with(holder.binding) {
attendanceItemNumber.text = item.number.toString()
attendanceItemSubject.text = item.subject
attendanceItemDescription.text = item.name
attendanceItemDescription.setText(item.description)
attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE }
attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseInfo.visibility = View.GONE

View File

@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogAttendanceBinding
import io.github.wulkanowy.utils.description
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
@ -43,7 +44,7 @@ class AttendanceDialog : DialogFragment() {
with(binding) {
attendanceDialogSubject.text = attendance.subject
attendanceDialogDescription.text = attendance.name
attendanceDialogDescription.setText(attendance.description)
attendanceDialogDate.text = attendance.date.toFormattedString()
attendanceDialogNumber.text = attendance.number.toString()
attendanceDialogClose.setOnClickListener { dismiss() }

View File

@ -164,8 +164,8 @@ class GradeStatisticsPresenter @Inject constructor(
Status.SUCCESS -> {
Timber.i("Loading grade stats result: Success")
view?.run {
showEmpty(it.data!!.isEmpty())
showContent(it.data.isNotEmpty())
showEmpty(it.data!!.isEmpty() || it.data.first().partial.isEmpty())
showContent(it.data.isNotEmpty() && it.data.first().partial.isNotEmpty())
showErrorView(false)
updateData(it.data, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList)
showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList)

View File

@ -21,7 +21,7 @@ class GradeSummaryAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerV
var items = emptyList<GradeSummary>()
override fun getItemCount() = items.size + 1
override fun getItemCount() = items.size + if (items.isNotEmpty()) 1 else 0
override fun getItemViewType(position: Int) = when (position) {
0 -> ViewType.HEADER.id

View File

@ -45,16 +45,17 @@ class GradeSummaryPresenter @Inject constructor(
Status.LOADING -> Timber.i("Loading grade summary started")
Status.SUCCESS -> {
Timber.i("Loading grade summary result: Success")
val items = createGradeSummaryItems(it.data!!)
view?.run {
showEmpty(it.data!!.isEmpty())
showContent(it.data.isNotEmpty())
showEmpty(items.isEmpty())
showContent(items.isNotEmpty())
showErrorView(false)
updateData(createGradeSummaryItems(it.data))
updateData(items)
}
analytics.logEvent(
"load_data",
"type" to "grade_summary",
"items" to it.data!!.size
"items" to it.data.size
)
}
Status.ERROR -> {

View File

@ -66,7 +66,12 @@ class LoginRecoverPresenter @Inject constructor(
showErrorView(false)
showCaptcha(false)
}
Status.SUCCESS -> view?.loadReCaptcha(siteKey = it.data!!.first, url = it.data.second)
Status.SUCCESS -> view?.run {
loadReCaptcha(url = it.data!!.first, siteKey = it.data.second)
showProgress(false)
showErrorView(false)
showCaptcha(true)
}
Status.ERROR -> {
Timber.i("Obtain captcha site key result: An exception occurred")
errorHandler.dispatch(it.error!!)

View File

@ -64,7 +64,8 @@ class MessagePreviewPresenter @Inject constructor(
when (it.status) {
Status.LOADING -> Timber.i("Loading message ${message.messageId} preview started")
Status.SUCCESS -> {
Timber.i("Loading message ${it.data!!.message.messageId} preview result: Success ")
Timber.i("Loading message ${message.messageId} preview result: Success ")
checkNotNull(it.data, { "Can't find message in local db! Probably no longer exist in this folder" })
this@MessagePreviewPresenter.message = it.data.message
this@MessagePreviewPresenter.attachments = it.data.attachments
view?.apply {
@ -194,6 +195,7 @@ class MessagePreviewPresenter @Inject constructor(
view?.run {
lastError = error
setErrorDetails(message)
showContent(false)
showErrorView(true)
setErrorRetryCallback { retryCallback() }
}

View File

@ -120,15 +120,6 @@ class SettingsFragment : PreferenceFragmentCompat(),
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showForceSyncDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.pref_services_dialog_force_sync_title)
.setMessage(R.string.pref_services_dialog_force_sync_summary)
.setPositiveButton(android.R.string.ok) { _, _ -> presenter.onForceSyncDialogSubmit() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
override fun showFixSyncDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.pref_notify_fix_sync_issues)

View File

@ -55,14 +55,6 @@ class SettingsPresenter @Inject constructor(
}
fun onSyncNowClicked() {
view?.showForceSyncDialog()
}
fun onFixSyncIssuesClicked() {
view?.showFixSyncDialog()
}
fun onForceSyncDialogSubmit() {
view?.run {
syncManager.startOneTimeSyncWorker().onEach { workInfo ->
when (workInfo.state) {
@ -87,4 +79,8 @@ class SettingsPresenter @Inject constructor(
}.launch("sync")
}
}
fun onFixSyncIssuesClicked() {
view?.showFixSyncDialog()
}
}

View File

@ -18,6 +18,5 @@ interface SettingsView : BaseView {
fun setSyncInProgress(inProgress: Boolean)
fun showForceSyncDialog()
fun showFixSyncDialog()
}

View File

@ -6,6 +6,7 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable
@ -44,8 +45,8 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
private val timers = mutableMapOf<Int, Timer>()
private fun resetTimers() {
Timber.d("Timetable timers reset")
fun resetTimers() {
Timber.d("Timetable timers (${timers.size}) reset")
with(timers) {
forEach { (_, timer) -> timer.cancel() }
clear()
@ -69,11 +70,6 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
resetTimers()
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val lesson = items[position]
@ -112,8 +108,12 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
bindNormalDescription(binding, lesson)
bindNormalColors(binding, lesson)
if (lesson.isStudentPlan && showTimers) timers[position] = timer(period = 1000) {
root.post { updateTimeLeft(binding, lesson, position) }
if (lesson.isStudentPlan && showTimers) {
timers[position] = timer(period = 1000) {
if (ViewCompat.isAttachedToWindow(root)) {
root.post { updateTimeLeft(binding, lesson, position) }
}
}
} else {
// reset item on set changed
timetableItemTimeUntil.visibility = GONE

View File

@ -185,6 +185,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
}
override fun onDestroyView() {
timetableAdapter.resetTimers()
presenter.onDetachView()
super.onDestroyView()
}

View File

@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.lang.NullPointerException
import java.time.LocalDate
import java.time.LocalDate.now
import java.time.LocalDate.of

View File

@ -1,6 +1,9 @@
package io.github.wulkanowy.utils
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory
/**
* [UONET+ - Zasady tworzenia podsumowań liczb uczniów obecnych i nieobecnych w tabeli frekwencji]
@ -23,4 +26,15 @@ private fun calculatePercentage(presence: Double, absence: Double): Double {
return if ((presence + absence) == 0.0) 0.0 else (presence / (presence + absence)) * 100
}
inline val Attendance.description
get() = when (AttendanceCategory.getCategoryByName(name)) {
AttendanceCategory.PRESENCE -> R.string.attendance_present
AttendanceCategory.ABSENCE_UNEXCUSED -> R.string.attendance_absence_unexcused
AttendanceCategory.ABSENCE_EXCUSED -> R.string.attendance_absence_excused
AttendanceCategory.UNEXCUSED_LATENESS -> R.string.attendance_unexcused_lateness
AttendanceCategory.EXCUSED_LATENESS -> R.string.attendance_excused_lateness
AttendanceCategory.ABSENCE_FOR_SCHOOL_REASONS -> R.string.attendance_absence_school
AttendanceCategory.EXEMPTION -> R.string.attendance_exemption
AttendanceCategory.DELETED -> R.string.attendance_deleted
else -> R.string.attendance_unknown
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.utils
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
@ -69,25 +70,23 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
fun <T> flowWithResource(block: suspend () -> T) = flow {
emit(Resource.loading())
try {
emit(Resource.success(block()))
emit(try {
Resource.success(block())
} catch (e: Throwable) {
emit(Resource.error(e))
}
Resource.error(e)
})
}
fun <T> flowWithResourceIn(block: suspend () -> Flow<Resource<T>>) = flow {
emit(Resource.loading())
try {
block().collect {
block()
.catch { emit(Resource.error(it)) }
.collect {
if (it.status != Status.LOADING) { // LOADING is already emitted
emit(it)
}
}
} catch (e: Throwable) {
emit(Resource.error(e))
}
}
fun <T> Flow<Resource<T>>.afterLoading(callback: () -> Unit) = onEach {

View File

@ -13,8 +13,7 @@ import java.time.Month
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter.ofPattern
import java.time.format.TextStyle.FULL_STANDALONE
import java.time.format.TextStyle.*
import java.time.format.TextStyle.FULL
import java.time.temporal.TemporalAdjusters.firstInMonth
import java.time.temporal.TemporalAdjusters.next
import java.time.temporal.TemporalAdjusters.previous
@ -78,6 +77,12 @@ inline val LocalDate.nextOrSameSchoolDay: LocalDate
}
}
inline val LocalDate.startExamsDay: LocalDate
get() = nextOrSameSchoolDay.monday
inline val LocalDate.endExamsDay: LocalDate
get() = nextOrSameSchoolDay.monday.plusWeeks(4).minusDays(1)
inline val LocalDate.previousOrSameSchoolDay: LocalDate
get() {
return when (dayOfWeek) {

View File

@ -1,5 +1,5 @@
Wersja 0.20.0
- naprawiliśmy obsługę wiadomości
- naprawiliśmy wyświetlanie oznaczenia klasy ucznia w menadżerze kont
Wersja 0.20.5
- naprawiliśmy logowanie do koszalińskiego dziennika
- naprawiliśmy resetowanie hasła na gdańskim dzienniku
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -168,6 +168,8 @@
<string name="attendance_excused_lateness">Spóźnienie usprawiedliwione</string>
<string name="attendance_unexcused_lateness">Spóźnienie nieusprawiedliwione</string>
<string name="attendance_present">Obecność</string>
<string name="attendance_deleted">Usunięty</string>
<string name="attendance_unknown">Nieznany</string>
<string name="attendance_number">Numer lekcji</string>
<string name="attendance_no_items">Brak wpisów</string>
<plurals name="attendance_number_absences">

View File

@ -7,9 +7,11 @@
<item>Lubelski Portal Oświatowy</item>
<item>EduNet Miasta Tarnowa</item>
<item>ResMan Rzeszów</item>
<item>Platforma Edukacyjna Koszalina</item>
<item>Rawa Mazowiecka - Platforma vEdukacja</item>
<item>Zduńska Wola - e-Urząd</item>
<item>Sieradz - Portal oświatowy</item>
<item>Skarżysko-Kamienna - e-Skarżysko</item>
<item>Łask - Platforma vEdukacja</item>
<item>Powiat łaski - Platforma edukacyjna</item>
<item>Powiat Krasnostawski - Platforma oświatowa</item>
@ -26,6 +28,8 @@
<item>https://edu.lublin.eu</item>
<item>https://umt.tarnow.pl</item>
<item>https://resman.pl</item>
<item>https://eduportal.koszalin.pl</item>
<item>https://vulcan.net.pl/</item>
<item>https://vulcan.net.pl/</item>
<item>https://vulcan.net.pl/</item>
<item>https://vulcan.net.pl/</item>
@ -45,9 +49,11 @@
<item>lublin</item>
<item>tarnow</item>
<item>rzeszow</item>
<item>koszalin</item>
<item>rawamazowiecka</item>
<item>zdunskawola</item>
<item>sieradz</item>
<item>skarzyskokamienna</item>
<item>lask</item>
<item>powiatlaski</item>
<item>powiatkrasnostawski</item>

View File

@ -170,6 +170,8 @@
<string name="attendance_excused_lateness">Excused lateness</string>
<string name="attendance_unexcused_lateness">Unexcused lateness</string>
<string name="attendance_present">Present</string>
<string name="attendance_deleted">Deleted</string>
<string name="attendance_unknown">Unknown</string>
<string name="attendance_number">Number of lesson</string>
<string name="attendance_no_items">No entries</string>
<plurals name="attendance_number_absences">

View File

@ -90,10 +90,10 @@ fun getMessageEntity(
date = now(),
folderId = 1,
unread = unread,
unreadBy = 1,
readBy = 1,
removed = false,
hasAttachments = false
).apply {
this.content = content
unreadBy = 1
readBy = 1
}

View File

@ -63,7 +63,15 @@ class SemesterRepositoryTest {
createSemesterEntity(0, 2, now().minusMonths(3), now())
)
val goodSemesters = listOf(
createSemesterEntity(122, 1, now().minusMonths(6), now().minusMonths(3)),
createSemesterEntity(123, 2, now().minusMonths(3), now())
)
coEvery { semesterLocal.getSemesters(student) } returns badSemesters
coEvery { semesterRemote.getSemesters(student) } returns goodSemesters
coEvery { semesterLocal.deleteSemesters(any()) } just Runs
coEvery { semesterLocal.saveSemesters(any()) } just Runs
val items = runBlocking { semesterRepository.getSemesters(student) }
assertEquals(2, items.size)
@ -152,12 +160,23 @@ class SemesterRepositoryTest {
@Test
fun getSemesters_noCurrent_refreshOnNoCurrent() {
val semesters = listOf(
val semestersWithNoCurrent = listOf(
createSemesterEntity(1, 1, now().minusMonths(12), now().minusMonths(6)),
createSemesterEntity(1, 2, now().minusMonths(6), now().minusMonths(1))
)
coEvery { semesterLocal.getSemesters(student) } returns semesters
val newSemesters = listOf(
createSemesterEntity(1, 1, now().minusMonths(12), now().minusMonths(6)),
createSemesterEntity(1, 2, now().minusMonths(6), now().minusMonths(1)),
createSemesterEntity(2, 1, now().minusMonths(1), now().plusMonths(5)),
createSemesterEntity(2, 2, now().plusMonths(5), now().plusMonths(11)),
)
coEvery { semesterLocal.getSemesters(student) } returns semestersWithNoCurrent
coEvery { semesterRemote.getSemesters(student) } returns newSemesters
coEvery { semesterLocal.deleteSemesters(any()) } just Runs
coEvery { semesterLocal.saveSemesters(any()) } just Runs
val items = runBlocking { semesterRepository.getSemesters(student, refreshOnNoCurrent = true) }
assertEquals(2, items.size)

View File

@ -171,4 +171,42 @@ class TimeExtensionTest {
assertEquals(of(2019, 5, 1), of(2019, 5, 1).getLastSchoolDayIfHoliday(2018))
assertEquals(of(2018, 5, 1), of(2019, 5, 1).getLastSchoolDayIfHoliday(2017))
}
@Test
fun getExamsCutOffDates() {
with(of(2020, 9, 13)) {
assertEquals(of(2020, 9, 14), startExamsDay)
assertEquals(of(2020, 10, 11), endExamsDay)
}
with(of(2020, 9, 14)) {
assertEquals(of(2020, 9, 14), startExamsDay)
assertEquals(of(2020, 10, 11), endExamsDay)
}
with(of(2020, 9, 15)) {
assertEquals(of(2020, 9, 14), startExamsDay)
assertEquals(of(2020, 10, 11), endExamsDay)
}
with(of(2020, 9, 16)) {
assertEquals(of(2020, 9, 14), startExamsDay)
assertEquals(of(2020, 10, 11), endExamsDay)
}
with(of(2020, 9, 17)) {
assertEquals(of(2020, 9, 14), startExamsDay)
assertEquals(of(2020, 10, 11), endExamsDay)
}
with(of(2020, 9, 18)) {
assertEquals(of(2020, 9, 14), startExamsDay)
assertEquals(of(2020, 10, 11), endExamsDay)
}
with(of(2020, 9, 19)) {
assertEquals(of(2020, 9, 21), startExamsDay)
assertEquals(of(2020, 10, 18), endExamsDay)
}
}
}

View File

@ -1,8 +1,8 @@
buildscript {
ext {
kotlin_version = '1.4.0'
kotlin_version = '1.4.10'
about_libraries = '8.3.0'
hilt_version = "2.28.3-alpha"
hilt_version = "2.29.1-alpha"
}
repositories {
mavenCentral()
@ -15,7 +15,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.google.gms:google-services:4.3.3'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'
classpath "com.github.triplet.gradle:play-publisher:2.7.5"
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.0"
classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0"