1
0

Compare commits

...

73 Commits
2.4.0 ... 2.5.5

Author SHA1 Message Date
a82e11d694 Merge branch 'bugfix/2.5.5' 2024-03-26 20:38:01 +01:00
4dc5fc65ac Version 2.5.5 2024-03-26 20:37:51 +01:00
7463cf6253 Replace function in DAO to 'OR' in SQL query 2024-03-26 20:29:35 +01:00
d799ec7ac9 Add skipping migration when previous attempt failed (#2508) 2024-03-26 19:19:44 +01:00
254719f22f Remove classId from semester query when eduOne (#2509) 2024-03-26 19:02:35 +01:00
e1e276e1ea Merge branch 'bugfix/2.5.4' 2024-03-26 13:29:26 +01:00
8cdd4311a9 Version 2.5.4 2024-03-26 13:29:22 +01:00
b7f7b16aef Add logging error from units and symbols during registration (#2507) 2024-03-26 12:54:07 +01:00
2e71c50894 Merge branch 'bugfix/2.5.3' 2024-03-24 23:43:51 +01:00
b3faac01a5 Version 2.5.3 2024-03-24 23:43:45 +01:00
3881678208 Fix issues related to not authenticated eduOne students (#2501) 2024-03-24 23:22:07 +01:00
76d038eefa Hide grade statistics when is eduOne, add isEduOne flag migration (#2496)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-03-24 19:42:36 +01:00
3a55c3c760 Add ignoring FeatureUnavailableException in SyncWorker (#2500) 2024-03-24 18:55:23 +01:00
a0818de7d1 Add isAdded condition to HomeworkAddDialog (#2499) 2024-03-24 14:55:05 +01:00
b280316b07 Bump sdk to 2.5.3-SNAPSHOT 2024-03-21 23:06:18 +01:00
0554aa91fd Add WulkanowySdkFactory (#2479) 2024-03-21 22:11:03 +01:00
5a77d1e940 Hide lesson number when is eduOne (#2498) 2024-03-21 21:19:49 +01:00
c9a42a6cf6 Add try catch to initialize MobileAds SDK (#2497) 2024-03-21 11:03:08 +01:00
e17129efea Merge branch 'bugfix/2.5.2' 2024-03-20 02:18:07 +01:00
6047af9ff0 Version 2.5.2 2024-03-20 02:18:00 +01:00
d789aa718e Change AuthDialog condition to isAuth flag (#2495)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-03-20 01:49:55 +01:00
8623b53357 Fix lateness color in attendance (#2481) 2024-03-19 22:13:32 +01:00
78e28ad791 Remove savedInstance in MessagePreviewFragment (#2477) 2024-03-19 22:13:09 +01:00
377c288e9e Add missing onDetachView in AutDialog (#2476) 2024-03-19 22:12:54 +01:00
b31c7e1720 Fix task description color crash (#2475) 2024-03-19 22:12:39 +01:00
d01fe9c370 Bump sdk to 2.5.2-SNAPSHOT 2024-03-19 22:11:02 +01:00
5ed19cb21a Merge branch 'release/2.5.1' 2024-03-03 11:19:42 +01:00
0a1f7270b4 Version 2.5.1 2024-03-03 11:15:11 +01:00
47d8513a77 New Crowdin updates (#2464) 2024-03-03 10:35:17 +01:00
00432ab911 Merge branch 'release/2.5.0' into develop 2024-03-02 21:18:14 +01:00
7b2c839775 Merge branch 'release/2.5.0' 2024-03-02 21:18:10 +01:00
f455064b9d Version 2.5.0 2024-03-02 21:18:02 +01:00
2bbc157d03 Add some new symbols to symbol autocomplete field (#2461) 2024-03-02 20:45:23 +01:00
a0a0b8dea6 New Crowdin updates (#2460) 2024-03-02 20:37:36 +01:00
3bab883a56 Add a message explaining the reason for the captcha to the captcha dialog (#2459) 2024-03-02 19:49:08 +01:00
b319bb03cd New Crowdin updates (#2458) 2024-03-02 17:31:44 +01:00
333306e7ba Wrap delete and save operations in database transactions (#2450) 2024-03-02 16:25:27 +00:00
fb240938ed Bump about_libraries from 10.10.0 to 11.1.0 (#2454) 2024-03-02 16:21:55 +00:00
dc9af29a44 Bump hilt_version from 2.50 to 2.51 (#2456) 2024-03-02 16:11:13 +00:00
e9d64de0cb Improve invalid password message (#2451) 2024-03-02 17:10:38 +01:00
05bda598fc Bump mockk from 1.13.9 to 1.13.10 (#2455) 2024-03-02 16:10:19 +00:00
3564366a8f Bump com.google.firebase:firebase-bom from 32.7.2 to 32.7.3 (#2453) 2024-03-02 16:08:32 +00:00
f2d26453ed Fix calculating average with optional arithmetic average on and no grade with average in second semester (#2448) 2024-03-02 17:01:12 +01:00
ccba31f2e8 Add last announcements to school announcements (#2452) 2024-03-02 16:55:54 +01:00
a7238e3f23 New Crowdin updates (#2447) 2024-03-01 22:16:56 +01:00
ea28fc783c Add message from trash restoring (#2438) 2024-03-01 21:14:43 +01:00
c04752ed39 Fix timetable items layout (#2446) 2024-03-01 10:32:55 +01:00
c198e6a2f7 New Crowdin updates (#2445) 2024-03-01 00:06:54 +01:00
2c1337bb51 New Crowdin updates (#2439) 2024-02-29 21:36:51 +01:00
7a4032dda4 Add mute message senders (#2415)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-02-29 21:30:02 +01:00
1ab300d74f Bump android_hilt from 1.1.0 to 1.2.0 (#2443) 2024-02-27 08:53:00 +00:00
1b8c389984 Bump io.coil-kt:coil from 2.5.0 to 2.6.0 (#2441) 2024-02-27 08:52:47 +00:00
74a20b2f65 Add Github Sponsor (#2444) 2024-02-27 09:42:44 +01:00
d5c17285c1 Fix error handling in login (#2437) 2024-02-25 16:37:28 +01:00
e378b4c70a Fix loading timetable and attendance when should be refreshed returns true (#2436) 2024-02-25 16:36:50 +01:00
31854fc4b8 Fix text cut off across the app when text size is set to 200% (#2435) 2024-02-25 16:35:56 +01:00
f52fe8306f Merge branch 'release/2.4.2' into develop 2024-02-22 16:15:36 +01:00
3eae3a7667 Merge branch 'release/2.4.2' 2024-02-22 16:15:30 +01:00
b613b84469 Version 2.4.2 2024-02-22 16:15:24 +01:00
2776d019b9 Revert "Bump com.google.android.ump:user-messaging-platform from 2.1.0 to 2.2…" (#2434)
This reverts commit fc91936884.
2024-02-22 15:52:40 +01:00
729e72cddb Fix text pasting into date field in additional lesson add dialog (#2433) 2024-02-21 21:36:20 +01:00
ec101c1f52 Fix error handling in widgets (#2430) 2024-02-19 11:30:05 +01:00
cfec79405f Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2429) 2024-02-18 14:21:59 +00:00
7d8be1b9fc Bump coroutines from 1.7.3 to 1.8.0 (#2428) 2024-02-18 14:21:38 +00:00
c781159e75 Merge branch 'release/2.4.1' into develop 2024-02-17 13:10:39 +01:00
90a5b9e20f Merge branch 'release/2.4.1' 2024-02-17 13:10:34 +01:00
3cf6c295b0 Version 2.4.1 2024-02-17 13:07:45 +01:00
e757585bd3 New Crowdin updates (#2426) 2024-02-17 12:43:52 +01:00
736d16a7ab Make WebkitCookieManagerProxy no-op if webview is not available on the device (#2427) 2024-02-17 12:31:14 +01:00
6f4a8d5534 Add missing symbol and custom domain suffix validation (#2425) 2024-02-17 11:41:32 +01:00
b5e17c4ff7 Bump com.google.firebase:firebase-bom from 32.7.1 to 32.7.2 (#2424) 2024-02-16 18:50:07 +00:00
cc01525f16 Bump com.google.gms:google-services from 4.4.0 to 4.4.1 (#2423) 2024-02-16 18:49:46 +00:00
c2ec05662b Merge branch 'release/2.4.0' into develop 2024-02-09 19:45:05 +01:00
158 changed files with 12948 additions and 1081 deletions

4
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: wulkanowy
custom: https://www.paypal.com/paypalme/wulkanowy

4
.gitignore vendored
View File

@ -67,6 +67,10 @@ captures/
.idea/discord.xml
.idea/migrations.xml
.idea/androidTestResultsUserPreferences.xml
.idea/copilot
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.idea/kotlinc.xml
# Keystore files
*.jks

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 146
versionName "2.4.0"
versionCode 154
versionName "2.5.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -164,7 +164,7 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.50d
userFraction = 0.99d
updatePriority = 1
enabled.set(false)
}
@ -187,19 +187,19 @@ huaweiPublish {
ext {
work_manager = "2.9.0"
android_hilt = "1.1.0"
android_hilt = "1.2.0"
room = "2.6.1"
chucker = "4.0.0"
mockk = "1.13.9"
coroutines = "1.7.3"
mockk = "1.13.10"
coroutines = "1.8.0"
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.4.0'
implementation 'io.github.wulkanowy:sdk:2.5.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation 'androidx.core:core-ktx:1.12.0'
@ -246,13 +246,13 @@ dependencies {
implementation 'com.github.Faierbel:slf4j-timber:2.0'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'io.coil-kt:coil:2.5.0'
implementation 'io.coil-kt:coil:2.6.0'
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.11.0'
playImplementation platform('com.google.firebase:firebase-bom:32.7.1')
playImplementation platform('com.google.firebase:firebase-bom:32.7.3')
playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:'
@ -262,7 +262,7 @@ dependencies {
playImplementation "com.google.android.play:integrity:1.3.0"
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
playImplementation 'com.google.android.play:review-ktx:2.0.1'
playImplementation "com.google.android.ump:user-messaging-platform:2.2.0"
playImplementation "com.google.android.ump:user-messaging-platform:2.1.0"
hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -54,5 +54,9 @@
{
"displayName": "Antoni Paduch",
"githubUsername": "janAte1"
},
{
"displayName": "Kamil Wąsik",
"githubUsername": "JestemKamil"
}
]

View File

@ -18,17 +18,13 @@ import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.create
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@ -36,20 +32,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal class DataModule {
@Singleton
@Provides
fun provideSdk(chuckerInterceptor: ChuckerInterceptor, remoteConfig: RemoteConfigHelper) =
Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(WebkitCookieManagerProxy())
// for debug only
addInterceptor(chuckerInterceptor, network = true)
}
@Singleton
@Provides
fun provideChuckerCollector(
@ -254,6 +236,10 @@ internal class DataModule {
@Provides
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
@Singleton
@Provides
fun provideMutesDao(database: AppDatabase) = database.mutedMessageSendersDao
@Singleton
@Provides
fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao

View File

@ -30,8 +30,15 @@ val <T> Resource<T>.dataOrNull: T?
get() = when (this) {
is Resource.Success -> this.data
is Resource.Intermediate -> this.data
is Resource.Loading -> null
is Resource.Error -> null
else -> null
}
val <T> Resource<T>.dataOrThrow: T
get() = when (this) {
is Resource.Success -> this.data
is Resource.Intermediate -> this.data
is Resource.Loading -> throw IllegalStateException("Resource is in loading state")
is Resource.Error -> throw this.error
}
val <T> Resource<T>.errorOrNull: Throwable?

View File

@ -0,0 +1,125 @@
package io.github.wulkanowy.data
import com.chuckerteam.chucker.api.ChuckerInterceptor
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WulkanowySdkFactory @Inject constructor(
private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao,
) {
private val eduOneMutex = Mutex()
private val migrationFailedStudentIds = mutableSetOf<Long>()
private val sdk = Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(webkitCookieManagerProxy)
// for debug only
addInterceptor(chuckerInterceptor, network = true)
}
fun create() = sdk
suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne)
}
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk {
return create().apply {
email = student.email
password = student.password
symbol = student.symbol
schoolSymbol = student.schoolSymbol
studentId = student.studentId
classId = student.classId
emptyCookieJarInterceptor = true
isEduOne = isStudentEduOne
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
mobileBaseUrl = student.mobileBaseUrl
} else {
scrapperBaseUrl = student.scrapperBaseUrl
domainSuffix = student.scrapperDomainSuffix
loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
}
mode = Sdk.Mode.valueOf(student.loginMode)
mobileBaseUrl = student.mobileBaseUrl
keyId = student.certificateKey
privatePem = student.privateKey
if (semester != null) {
diaryId = semester.diaryId
kindergartenDiaryId = semester.kindergartenDiaryId
schoolYear = semester.schoolYear
unitId = semester.unitId
}
}
}
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
if (student.isEduOne != null) return student.isEduOne
if (student.id in migrationFailedStudentIds) {
Timber.i("Migration eduOne: skipping because of previous failure")
return false
}
eduOneMutex.withLock {
if (student.id in migrationFailedStudentIds) {
Timber.i("Migration eduOne: skipping because of previous failure")
return false
}
val studentFromDatabase = studentDb.loadById(student.id)
if (studentFromDatabase?.isEduOne != null) {
Timber.i("Migration eduOne: already done")
return studentFromDatabase.isEduOne
}
Timber.i("Migration eduOne: flag missing. Running migration...")
val initializedSdk = buildSdk(
student = student,
semester = null,
isStudentEduOne = false, // doesn't matter
)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Migration eduOne: can't get current student") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.i("Migration eduOne: failed, so skipping")
migrationFailedStudentIds.add(student.id)
return false
}
Timber.i("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
val studentIsEduOne = StudentIsEduOne(
id = student.id,
isEduOne = newCurrentStudent.isEduOne
)
studentDb.update(studentIsEduOne)
return newCurrentStudent.isEduOne
}
}
}

View File

@ -25,6 +25,7 @@ import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.dao.RecipientDao
@ -56,6 +57,7 @@ import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.MutedMessageSender
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Recipient
@ -118,6 +120,7 @@ import io.github.wulkanowy.data.db.migrations.Migration55
import io.github.wulkanowy.data.db.migrations.Migration57
import io.github.wulkanowy.data.db.migrations.Migration58
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration63
import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9
@ -157,6 +160,7 @@ import javax.inject.Singleton
SchoolAnnouncement::class,
Notification::class,
AdminMessage::class,
MutedMessageSender::class,
GradeDescriptive::class,
],
autoMigrations = [
@ -169,6 +173,10 @@ import javax.inject.Singleton
AutoMigration(from = 56, to = 57, spec = Migration57::class),
AutoMigration(from = 57, to = 58, spec = Migration58::class),
AutoMigration(from = 58, to = 59),
AutoMigration(from = 59, to = 60),
AutoMigration(from = 60, to = 61),
AutoMigration(from = 61, to = 62),
AutoMigration(from = 62, to = 63, spec = Migration63::class),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -177,7 +185,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 59
const val VERSION_SCHEMA = 63
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -303,5 +311,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract val adminMessagesDao: AdminMessageDao
abstract val mutedMessageSendersDao: MutedMessageSendersDao
abstract val gradeDescriptiveDao: GradeDescriptiveDao
}

View File

@ -2,24 +2,14 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.AdminMessage
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
abstract class AdminMessageDao : BaseDao<AdminMessage> {
interface AdminMessageDao : BaseDao<AdminMessage> {
@Query("SELECT * FROM AdminMessages")
abstract fun loadAll(): Flow<List<AdminMessage>>
@Transaction
open suspend fun removeOldAndSaveNew(
oldMessages: List<AdminMessage>,
newMessages: List<AdminMessage>
) {
deleteAll(oldMessages)
insertAll(newMessages)
}
fun loadAll(): Flow<List<AdminMessage>>
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Transaction
import androidx.room.Update
interface BaseDao<T> {
@ -15,4 +16,10 @@ interface BaseDao<T> {
@Delete
suspend fun deleteAll(items: List<T>)
@Transaction
suspend fun removeOldAndSaveNew(oldItems: List<T>, newItems: List<T>) {
deleteAll(oldItems)
insertAll(newItems)
}
}

View File

@ -5,15 +5,23 @@ import androidx.room.Query
import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import kotlinx.coroutines.flow.Flow
@Dao
interface MessagesDao : BaseDao<Message> {
@Transaction
@Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?>
@Transaction
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadMessagesWithMutedAuthor(mailboxKey: String, folder: Int): Flow<List<MessageWithMutedAuthor>>
@Transaction
@Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC")
fun loadMessagesWithMutedAuthor(folder: Int, email: String): Flow<List<MessageWithMutedAuthor>>
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>

View File

@ -0,0 +1,20 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.MutedMessageSender
@Dao
interface MutedMessageSendersDao : BaseDao<MutedMessageSender> {
@Query("SELECT COUNT(*) FROM MutedMessageSenders WHERE author = :author")
suspend fun checkMute(author: String): Boolean
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertMute(mute: MutedMessageSender): Long
@Query("DELETE FROM MutedMessageSenders WHERE author = :author")
suspend fun deleteMute(author: String)
}

View File

@ -14,6 +14,6 @@ interface SemesterDao : BaseDao<Semester> {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertSemesters(items: List<Semester>): List<Long>
@Query("SELECT * FROM Semesters WHERE student_id = :studentId AND class_id = :classId")
@Query("SELECT * FROM Semesters WHERE (student_id = :studentId AND class_id = :classId) OR (student_id = :studentId AND class_id = 0)")
suspend fun loadAll(studentId: Int, classId: Int): List<Semester>
}

View File

@ -9,6 +9,8 @@ import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import javax.inject.Singleton
@ -23,6 +25,12 @@ abstract class StudentDao {
@Delete
abstract suspend fun delete(student: Student)
@Update(entity = Student::class)
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
@Update(entity = Student::class)
abstract suspend fun update(studentIsEduOne: StudentIsEduOne)
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@ -39,11 +47,11 @@ abstract class StudentDao {
abstract suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0)")
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0) WHERE Students.id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")

View File

@ -15,5 +15,5 @@ interface TimetableDao : BaseDao<Timetable> {
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<Timetable>>
@Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List<Timetable>
suspend fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List<Timetable>
}

View File

@ -2,11 +2,15 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded
import androidx.room.Relation
import java.io.Serializable
data class MessageWithAttachment(
@Embedded
val message: Message,
@Relation(parentColumn = "message_global_key", entityColumn = "message_global_key")
val attachments: List<MessageAttachment>
)
val attachments: List<MessageAttachment>,
@Relation(parentColumn = "correspondents", entityColumn = "author")
val mutedMessageSender: MutedMessageSender?,
) : Serializable

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded
import androidx.room.Relation
data class MessageWithMutedAuthor(
@Embedded
val message: Message,
@Relation(parentColumn = "correspondents", entityColumn = "author")
val mutedMessageSender: MutedMessageSender?,
)

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity(tableName = "MutedMessageSenders")
data class MutedMessageSender(
@ColumnInfo(name = "author")
val author: String,
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -16,7 +16,9 @@ data class SchoolAnnouncement(
val subject: String,
val content: String
val content: String,
val author: String? = null,
) : Serializable {
@PrimaryKey(autoGenerate = true)

View File

@ -78,6 +78,13 @@ data class Student(
@ColumnInfo(name = "registration_date")
val registrationDate: Instant,
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable {
@PrimaryKey(autoGenerate = true)
@ -88,3 +95,22 @@ data class Student(
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
}
@Entity
data class StudentIsAuthorized(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_authorized", defaultValue = "NULL")
val isAuthorized: Boolean?,
) : Serializable
@Entity
data class StudentIsEduOne(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration63 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL("UPDATE Students SET is_edu_one = NULL WHERE is_edu_one = 0")
}
}

View File

@ -3,5 +3,10 @@ package io.github.wulkanowy.data.enums
enum class MessageFolder(val id: Int = 1) {
RECEIVED(1),
SENT(2),
TRASHED(3)
TRASHED(3),
;
companion object {
fun byId(id: Int) = entries.first { it.id == id }
}
}

View File

@ -3,12 +3,26 @@ package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformation
import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
@JvmName("mapDirectorInformationToEntities")
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
content = it.content,
author = null,
)
}
@JvmName("mapLastAnnouncementsToEntities")
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
content = it.content,
author = it.author,
)
}

View File

@ -34,17 +34,19 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerSubject ->
.map { registerStudent ->
RegisterStudent(
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapToEntities(registerSubject.studentId),
studentId = registerStudent.studentId,
studentName = registerStudent.studentName,
studentSecondName = registerStudent.studentSecondName,
studentSurname = registerStudent.studentSurname,
className = registerStudent.className,
classId = registerStudent.classId,
isParent = registerStudent.isParent,
isAuthorized = registerStudent.isAuthorized,
isEduOne = registerStudent.isEduOne,
semesters = registerStudent.semesters
.mapToEntities(registerStudent.studentId),
)
},
)
@ -84,6 +86,8 @@ fun RegisterStudent.mapToStudentWithSemesters(
password = user.password.orEmpty(),
isCurrent = false,
registrationDate = Instant.now(),
isAuthorized = this.isAuthorized,
isEduOne = this.isEduOne,
).apply {
avatarColor = colors.random()
},

View File

@ -45,4 +45,6 @@ data class RegisterStudent(
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
val isAuthorized: Boolean,
val isEduOne: Boolean
) : java.io.Serializable

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Attendance
@ -7,19 +8,14 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Absent
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
@ -30,7 +26,7 @@ import javax.inject.Singleton
class AttendanceRepository @Inject constructor(
private val attendanceDb: AttendanceDao,
private val timetableDb: TimetableDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -58,23 +54,21 @@ class AttendanceRepository @Inject constructor(
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
},
fetch = {
val lessons = withContext(Dispatchers.IO) {
timetableDb.load(
semester.diaryId, semester.studentId, start.monday, end.sunday
)
}
sdk.init(student)
.switchSemester(semester)
val lessons = timetableDb.load(
semester.diaryId, semester.studentId, start.monday, end.sunday
)
wulkanowySdkFactory.create(student, semester)
.getAttendance(start.monday, end.sunday)
.mapToEntities(semester, lessons)
},
saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract new)
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance ->
newAttendance.apply { if (notify) isNotified = false }
}
attendanceDb.insertAll(attendanceToAdd)
attendanceDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = attendanceToAdd,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
},
filterResult = { it.filter { item -> item.date in start..end } }
@ -93,8 +87,10 @@ class AttendanceRepository @Inject constructor(
}
suspend fun excuseForAbsence(
student: Student, semester: Semester,
absenceList: List<Attendance>, reason: String? = null
student: Student,
semester: Semester,
absenceList: List<Attendance>,
reason: String? = null
) {
val items = absenceList.map { attendance ->
Absent(
@ -102,8 +98,7 @@ class AttendanceRepository @Inject constructor(
timeId = attendance.timeId
)
}
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.excuseForAbsence(items, reason)
}
}

View File

@ -1,15 +1,15 @@
package io.github.wulkanowy.data.repositories
import androidx.room.withTransaction
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@ -18,8 +18,9 @@ import javax.inject.Singleton
@Singleton
class AttendanceSummaryRepository @Inject constructor(
private val attendanceDb: AttendanceSummaryDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
private val appDatabase: AppDatabase,
private val wulkanowySdkFactory: WulkanowySdkFactory,
) {
private val saveFetchResultMutex = Mutex()
@ -40,14 +41,15 @@ class AttendanceSummaryRepository @Inject constructor(
},
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getAttendanceSummary(subjectId)
.mapToEntities(semester, subjectId)
},
saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract new)
attendanceDb.insertAll(new uniqueSubtract old)
appDatabase.withTransaction {
attendanceDb.deleteAll(old uniqueSubtract new)
attendanceDb.insertAll(new uniqueSubtract old)
}
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)

View File

@ -1,12 +1,16 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.monday
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
@ -15,7 +19,7 @@ import javax.inject.Singleton
@Singleton
class CompletedLessonsRepository @Inject constructor(
private val completedLessonsDb: CompletedLessonsDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -47,14 +51,15 @@ class CompletedLessonsRepository @Inject constructor(
)
},
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getCompletedLessons(start.monday, end.sunday)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
completedLessonsDb.deleteAll(old uniqueSubtract new)
completedLessonsDb.insertAll(new uniqueSubtract old)
completedLessonsDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
},
filterResult = { it.filter { item -> item.date in start..end } }

View File

@ -1,16 +1,14 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
@ -21,7 +19,7 @@ import javax.inject.Singleton
@Singleton
class ConferenceRepository @Inject constructor(
private val conferenceDb: ConferenceDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -46,19 +44,18 @@ class ConferenceRepository @Inject constructor(
conferenceDb.loadAll(semester.diaryId, student.studentId, startDate)
},
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getConferences()
.mapToEntities(semester)
.filter { it.date >= startDate }
},
saveFetchResult = { old, new ->
val conferencesToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
conferenceDb.deleteAll(old uniqueSubtract new)
conferenceDb.insertAll(conferencesToSave)
conferenceDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
},
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)

View File

@ -1,18 +1,16 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.endExamsDay
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.startExamsDay
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
@ -23,7 +21,7 @@ import javax.inject.Singleton
@Singleton
class ExamRepository @Inject constructor(
private val examDb: ExamDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -56,18 +54,17 @@ class ExamRepository @Inject constructor(
)
},
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getExams(start.startExamsDay, start.endExamsDay)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
val examsToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
examDb.deleteAll(old uniqueSubtract new)
examDb.insertAll(examsToSave)
examDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
},
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
},
filterResult = { it.filter { item -> item.date in start..end } }

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
@ -10,11 +11,8 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.toLocalDate
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
@ -30,7 +28,7 @@ class GradeRepository @Inject constructor(
private val gradeDb: GradeDao,
private val gradeSummaryDb: GradeSummaryDao,
private val gradeDescriptiveDb: GradeDescriptiveDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -63,8 +61,7 @@ class GradeRepository @Inject constructor(
}
},
fetch = {
val (details, summary, descriptive) = sdk.init(student)
.switchSemester(semester)
val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
.getGrades(semester.semesterId)
Triple(
@ -87,10 +84,12 @@ class GradeRepository @Inject constructor(
new: List<GradeDescriptive>,
notify: Boolean
) {
gradeDescriptiveDb.deleteAll(old uniqueSubtract new)
gradeDescriptiveDb.insertAll((new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
})
gradeDescriptiveDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
},
)
}
private suspend fun refreshGradeDetails(
@ -101,13 +100,16 @@ class GradeRepository @Inject constructor(
) {
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date
?: student.registrationDate.toLocalDate()
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
if (it.date >= notifyBreakDate) it.apply {
isRead = false
if (notify) isNotified = false
}
})
gradeDb.removeOldAndSaveNew(
oldItems = oldGrades uniqueSubtract newDetails,
newItems = (newDetails uniqueSubtract oldGrades).onEach {
if (it.date >= notifyBreakDate) it.apply {
isRead = false
if (notify) isNotified = false
}
},
)
}
private suspend fun refreshGradeSummaries(
@ -115,31 +117,43 @@ class GradeRepository @Inject constructor(
newSummary: List<GradeSummary>,
notify: Boolean
) {
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject }
summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
else -> true
}
summary.isFinalGradeNotified = when {
summary.finalGrade.isEmpty() -> true
notify && oldSummary?.finalGrade != summary.finalGrade -> false
else -> true
}
gradeSummaryDb.removeOldAndSaveNew(
oldItems = oldSummaries uniqueSubtract newSummary,
newItems = (newSummary uniqueSubtract oldSummaries).onEach { summary ->
getGradeSummaryWithUpdatedNotificationState(
summary = summary,
oldSummary = oldSummaries.find { it.subject == summary.subject },
notify = notify,
)
},
)
}
summary.predictedGradeLastChange = when {
oldSummary == null -> Instant.now()
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
else -> oldSummary.predictedGradeLastChange
}
summary.finalGradeLastChange = when {
oldSummary == null -> Instant.now()
summary.finalGrade != oldSummary.finalGrade -> Instant.now()
else -> oldSummary.finalGradeLastChange
}
})
private fun getGradeSummaryWithUpdatedNotificationState(
summary: GradeSummary,
oldSummary: GradeSummary?,
notify: Boolean,
) {
summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
else -> true
}
summary.isFinalGradeNotified = when {
summary.finalGrade.isEmpty() -> true
notify && oldSummary?.finalGrade != summary.finalGrade -> false
else -> true
}
summary.predictedGradeLastChange = when {
oldSummary == null -> Instant.now()
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
else -> oldSummary.predictedGradeLastChange
}
summary.finalGradeLastChange = when {
oldSummary == null -> Instant.now()
summary.finalGrade != oldSummary.finalGrade -> Instant.now()
else -> oldSummary.finalGradeLastChange
}
}
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao
import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao
@ -12,14 +13,11 @@ import io.github.wulkanowy.data.mappers.mapPointsToStatisticsItems
import io.github.wulkanowy.data.mappers.mapSemesterToStatisticItems
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.util.*
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@ -28,7 +26,7 @@ class GradeStatisticsRepository @Inject constructor(
private val gradePartialStatisticsDb: GradePartialStatisticsDao,
private val gradePointsStatisticsDb: GradePointsStatisticsDao,
private val gradeSemesterStatisticsDb: GradeSemesterStatisticsDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -56,14 +54,15 @@ class GradeStatisticsRepository @Inject constructor(
},
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getGradesPartialStatistics(semester.semesterId)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
gradePartialStatisticsDb.deleteAll(old uniqueSubtract new)
gradePartialStatisticsDb.insertAll(new uniqueSubtract old)
gradePartialStatisticsDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(partialCacheKey, semester))
},
mapResult = { items ->
@ -80,6 +79,7 @@ class GradeStatisticsRepository @Inject constructor(
)
listOf(summaryItem) + items
}
else -> items.filter { it.subject == subjectName }
}.mapPartialToStatisticItems()
}
@ -101,14 +101,15 @@ class GradeStatisticsRepository @Inject constructor(
},
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getGradesSemesterStatistics(semester.semesterId)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
gradeSemesterStatisticsDb.deleteAll(old uniqueSubtract new)
gradeSemesterStatisticsDb.insertAll(new uniqueSubtract old)
gradeSemesterStatisticsDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(semesterCacheKey, semester))
},
mapResult = { items ->
@ -138,6 +139,7 @@ class GradeStatisticsRepository @Inject constructor(
}
listOf(summaryItem) + itemsWithAverage
}
else -> itemsWithAverage.filter { it.subject == subjectName }
}.mapSemesterToStatisticItems()
}
@ -157,14 +159,15 @@ class GradeStatisticsRepository @Inject constructor(
},
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getGradesPointsStatistics(semester.semesterId)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
gradePointsStatisticsDb.deleteAll(old uniqueSubtract new)
gradePointsStatisticsDb.insertAll(new uniqueSubtract old)
gradePointsStatisticsDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(pointsCacheKey, semester))
},
mapResult = { items ->

View File

@ -1,18 +1,16 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
@ -22,7 +20,7 @@ import javax.inject.Singleton
@Singleton
class HomeworkRepository @Inject constructor(
private val homeworkDb: HomeworkDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -55,20 +53,19 @@ class HomeworkRepository @Inject constructor(
)
},
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getHomework(start.monday, end.sunday)
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
val homeWorkToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
val filteredOld = old.filterNot { it.isAddedByUser }
homeworkDb.deleteAll(filteredOld uniqueSubtract new)
homeworkDb.insertAll(homeWorkToSave)
homeworkDb.removeOldAndSaveNew(
oldItems = filteredOld uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
},
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}
)

View File

@ -1,12 +1,11 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
@ -18,7 +17,7 @@ import javax.inject.Singleton
@Singleton
class LuckyNumberRepository @Inject constructor(
private val luckyNumberDb: LuckyNumberDao,
private val sdk: Sdk
private val wulkanowySdkFactory: WulkanowySdkFactory,
) {
private val saveFetchResultMutex = Mutex()
@ -33,17 +32,18 @@ class LuckyNumberRepository @Inject constructor(
shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) },
fetch = {
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
wulkanowySdkFactory.create(student)
.getLuckyNumber(student.schoolShortName)
?.mapToEntity(student)
},
saveFetchResult = { oldLuckyNumber, newLuckyNumber ->
newLuckyNumber ?: return@networkBoundResource
if (newLuckyNumber != oldLuckyNumber) {
val updatedLuckNumberList =
listOf(newLuckyNumber.apply { if (notify) isNotified = false })
oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
luckyNumberDb.insertAll(updatedLuckNumberList)
luckyNumberDb.removeOldAndSaveNew(
oldItems = listOfNotNull(oldLuckyNumber),
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
)
}
}
)

View File

@ -4,17 +4,22 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MutedMessageSendersDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import io.github.wulkanowy.data.db.entities.MutedMessageSender
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.SENT
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities
@ -22,16 +27,14 @@ import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -42,8 +45,9 @@ import javax.inject.Singleton
@Singleton
class MessageRepository @Inject constructor(
private val messagesDb: MessagesDao,
private val mutedMessageSendersDao: MutedMessageSendersDao,
private val messageAttachmentDao: MessageAttachmentDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
@ApplicationContext private val context: Context,
private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider,
@ -51,7 +55,6 @@ class MessageRepository @Inject constructor(
private val mailboxDao: MailboxDao,
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
) {
private val saveFetchResultMutex = Mutex()
private val messagesCacheKey = "message"
@ -63,7 +66,7 @@ class MessageRepository @Inject constructor(
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
): Flow<Resource<List<Message>>> = networkBoundResource(
): Flow<Resource<List<MessageWithMutedAuthor>>> = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() },
shouldFetch = {
@ -74,21 +77,30 @@ class MessageRepository @Inject constructor(
},
query = {
if (mailbox == null) {
messagesDb.loadAll(folder.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, folder.id)
messagesDb.loadMessagesWithMutedAuthor(folder.id, student.email)
} else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id)
},
fetch = {
sdk.init(student).getMessages(
folder = Folder.valueOf(folder.name),
mailboxKey = mailbox?.globalKey,
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
wulkanowySdkFactory.create(student)
.getMessages(
folder = Folder.valueOf(folder.name),
mailboxKey = mailbox?.globalKey,
)
.mapToEntities(
student = student,
mailbox = mailbox,
allMailboxes = mailboxDao.loadAll(student.email)
)
},
saveFetchResult = { old, new ->
messagesDb.deleteAll(old uniqueSubtract new)
messagesDb.insertAll((new uniqueSubtract old).onEach {
it.isNotified = !notify
})
saveFetchResult = { oldWithAuthors, new ->
val old = oldWithAuthors.map { it.message }
messagesDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
val muted = isMuted(it.correspondents)
it.isNotified = !notify || muted
},
)
refreshHelper.updateLastRefreshTimestamp(
getRefreshKey(messagesCacheKey, mailbox, folder)
)
@ -106,14 +118,13 @@ class MessageRepository @Inject constructor(
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
(it.message.unread && markAsRead) || it.message.content.isBlank()
},
query = {
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
},
query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
fetch = {
sdk.init(student).getMessageDetails(
messageKey = it!!.message.messageGlobalKey,
markAsRead = message.unread && markAsRead,
)
wulkanowySdkFactory.create(student)
.getMessageDetails(
messageKey = it!!.message.messageGlobalKey,
markAsRead = message.unread && markAsRead,
)
},
saveFetchResult = { old, new ->
checkNotNull(old) { "Fetched message no longer exist!" }
@ -152,22 +163,36 @@ class MessageRepository @Inject constructor(
subject: String,
content: String,
recipients: List<Recipient>,
mailboxId: String,
mailbox: Mailbox,
) {
sdk.init(student).sendMessage(
subject = subject,
content = content,
recipients = recipients.mapFromEntities(),
mailboxId = mailboxId,
)
wulkanowySdkFactory.create(student)
.sendMessage(
subject = subject,
content = content,
recipients = recipients.mapFromEntities(),
mailboxId = mailbox.globalKey,
)
refreshFolders(student, mailbox, listOf(SENT))
}
suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
wulkanowySdkFactory.create(student)
.restoreMessages(messages = messages.map { it.messageGlobalKey })
refreshFolders(student, mailbox)
}
suspend fun deleteMessage(student: Student, message: Message) {
deleteMessages(student, listOf(message))
}
suspend fun deleteMessages(student: Student, messages: List<Message>) {
val firstMessage = messages.first()
sdk.init(student).deleteMessages(
messages = messages.map { it.messageGlobalKey },
removeForever = firstMessage.folderId == TRASHED.id,
)
wulkanowySdkFactory.create(student)
.deleteMessages(
messages = messages.map { it.messageGlobalKey },
removeForever = firstMessage.folderId == TRASHED.id,
)
if (firstMessage.folderId != TRASHED.id) {
val deletedMessages = messages.map {
@ -181,18 +206,24 @@ class MessageRepository @Inject constructor(
}
messagesDb.updateAll(deletedMessages)
} else messagesDb.deleteAll(messages)
getMessages(
student = student,
mailbox = mailbox,
folder = TRASHED,
forceRefresh = true,
).first()
} else {
messagesDb.deleteAll(messages)
}
}
suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
deleteMessages(student, mailbox, listOf(message))
private suspend fun refreshFolders(
student: Student,
mailbox: Mailbox?,
folders: List<MessageFolder> = MessageFolder.entries
) {
folders.forEach {
getMessages(
student = student,
mailbox = mailbox,
folder = it,
forceRefresh = true,
).toFirstResult()
}
}
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
@ -206,7 +237,9 @@ class MessageRepository @Inject constructor(
},
query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) },
fetch = {
sdk.init(student).getMailboxes().mapToEntities(student)
wulkanowySdkFactory.create(student)
.getMailboxes()
.mapToEntities(student)
},
saveFetchResult = { old, new ->
mailboxDao.deleteAll(old uniqueSubtract new)
@ -236,4 +269,18 @@ class MessageRepository @Inject constructor(
context.getString(R.string.pref_key_message_draft),
value?.let { json.encodeToString(it) }
)
private suspend fun isMuted(author: String): Boolean {
return mutedMessageSendersDao.checkMute(author)
}
suspend fun muteMessage(author: String) {
if (isMuted(author)) return
mutedMessageSendersDao.insertMute(MutedMessageSender(author))
}
suspend fun unmuteMessage(author: String) {
if (!isMuted(author)) return
mutedMessageSendersDao.deleteMute(author)
}
}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Semester
@ -8,11 +9,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToMobileDeviceToken
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.MobileDeviceToken
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@ -21,7 +19,7 @@ import javax.inject.Singleton
@Singleton
class MobileDeviceRepository @Inject constructor(
private val mobileDb: MobileDeviceDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -42,30 +40,28 @@ class MobileDeviceRepository @Inject constructor(
},
query = { mobileDb.loadAll(student.userLoginId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getRegisteredDevices()
.mapToEntities(student)
},
saveFetchResult = { old, new ->
mobileDb.deleteAll(old uniqueSubtract new)
mobileDb.insertAll(new uniqueSubtract old)
mobileDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)
suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.unregisterDevice(device.deviceId)
mobileDb.deleteAll(listOf(device))
}
suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken {
return sdk.init(student)
.switchSemester(semester)
return wulkanowySdkFactory.create(student, semester)
.getToken()
.mapToMobileDeviceToken()
}

View File

@ -1,13 +1,16 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.toLocalDate
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@ -16,7 +19,7 @@ import javax.inject.Singleton
@Singleton
class NoteRepository @Inject constructor(
private val noteDb: NoteDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -40,20 +43,21 @@ class NoteRepository @Inject constructor(
},
query = { noteDb.loadAll(student.studentId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getNotes()
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
noteDb.deleteAll(old uniqueSubtract new)
noteDb.insertAll((new uniqueSubtract old).onEach {
val notesToAdd = (new uniqueSubtract old).onEach {
if (it.date >= student.registrationDate.toLocalDate()) it.apply {
isRead = false
if (notify) isNotified = false
}
})
}
noteDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = notesToAdd,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)

View File

@ -1,12 +1,15 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject
import javax.inject.Singleton
@ -14,19 +17,22 @@ import javax.inject.Singleton
@Singleton
class RecipientRepository @Inject constructor(
private val recipientDb: RecipientDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
private val cacheKey = "recipient"
suspend fun refreshRecipients(student: Student, mailbox: Mailbox, type: MailboxType) {
val new = sdk.init(student).getRecipients(mailbox.globalKey)
val new = wulkanowySdkFactory.create(student)
.getRecipients(mailbox.globalKey)
.mapToEntities(mailbox.globalKey)
val old = recipientDb.loadAll(type, mailbox.globalKey)
recipientDb.deleteAll(old uniqueSubtract new)
recipientDb.insertAll(new uniqueSubtract old)
recipientDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
@ -54,7 +60,7 @@ class RecipientRepository @Inject constructor(
): List<Recipient> {
mailbox ?: return emptyList()
return sdk.init(student)
return wulkanowySdkFactory.create(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)

View File

@ -1,17 +1,23 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.data.WulkanowySdkFactory
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecoverRepository @Inject constructor(private val sdk: Sdk) {
class RecoverRepository @Inject constructor(
private val wulkanowySdkFactory: WulkanowySdkFactory
) {
suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair<String, String> {
return sdk.getPasswordResetCaptchaCode(host, symbol)
}
suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair<String, String> =
wulkanowySdkFactory.create()
.getPasswordResetCaptchaCode(host, symbol)
suspend fun sendRecoverRequest(
url: String, symbol: String, email: String, reCaptchaResponse: String
): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
url: String,
symbol: String,
email: String,
reCaptchaResponse: String
): String = wulkanowySdkFactory.create()
.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
}

View File

@ -1,14 +1,13 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
@ -18,7 +17,7 @@ import javax.inject.Singleton
@Singleton
class SchoolAnnouncementRepository @Inject constructor(
private val schoolAnnouncementDb: SchoolAnnouncementDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -41,17 +40,18 @@ class SchoolAnnouncementRepository @Inject constructor(
schoolAnnouncementDb.loadAll(student.userLoginId)
},
fetch = {
sdk.init(student)
.getDirectorInformation()
.mapToEntities(student)
val sdk = wulkanowySdkFactory.create(student)
val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student)
val directorInformation = sdk.getDirectorInformation().mapToEntities(student)
lastAnnouncements + directorInformation
},
saveFetchResult = { old, new ->
val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
schoolAnnouncementDb.deleteAll(old uniqueSubtract new)
schoolAnnouncementDb.insertAll(schoolAnnouncementsToSave)
schoolAnnouncementDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
},
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)

View File

@ -1,15 +1,13 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -17,7 +15,7 @@ import javax.inject.Singleton
@Singleton
class SchoolRepository @Inject constructor(
private val schoolDb: SchoolDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -40,17 +38,16 @@ class SchoolRepository @Inject constructor(
},
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getSchool()
.mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(schoolDb) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
schoolDb.removeOldAndSaveNew(
oldItems = listOf(old),
newItems = listOf(new)
)
} else if (old == null) {
schoolDb.insertAll(listOf(new))
}

View File

@ -1,17 +1,15 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.IntegrityHelper
import io.github.wulkanowy.utils.getCurrentOrLast
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import java.util.UUID
@ -23,7 +21,7 @@ import kotlin.time.Duration.Companion.seconds
class SchoolsRepository @Inject constructor(
private val integrityHelper: IntegrityHelper,
private val schoolsService: SchoolsService,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
) {
suspend fun logSchoolLogin(loginData: LoginData, students: List<StudentWithSemesters>) {
@ -40,10 +38,9 @@ class SchoolsRepository @Inject constructor(
private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) {
val requestId = UUID.randomUUID().toString()
val token = integrityHelper.getIntegrityToken(requestId) ?: return
val updatedStudent = student.copy(password = loginData.password)
val schoolInfo = sdk
.init(student.copy(password = loginData.password))
.switchSemester(semester)
val schoolInfo = wulkanowySdkFactory.create(updatedStudent, semester)
.getSchool()
schoolsService.logLoginEvent(

View File

@ -1,11 +1,15 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.DispatchersProvider
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
@ -14,8 +18,8 @@ import javax.inject.Singleton
@Singleton
class SemesterRepository @Inject constructor(
private val semesterDb: SemesterDao,
private val sdk: Sdk,
private val dispatchers: DispatchersProvider
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val dispatchers: DispatchersProvider,
) {
suspend fun getSemesters(
@ -45,6 +49,7 @@ class SemesterRepository @Inject constructor(
0 == it.diaryId && 0 == it.kindergartenDiaryId
} == true
}
else -> false
}
@ -55,12 +60,20 @@ class SemesterRepository @Inject constructor(
}
private suspend fun refreshSemesters(student: Student) {
val new = sdk.init(student).getSemesters().mapToEntities(student.studentId)
if (new.isEmpty()) return Timber.i("Empty semester list!")
val new = wulkanowySdkFactory.create(student)
.getSemesters()
.mapToEntities(student.studentId)
if (new.isEmpty()) {
Timber.i("Empty semester list from SDK!")
return
}
val old = semesterDb.loadAll(student.studentId, student.classId)
semesterDb.deleteAll(old.uniqueSubtract(new))
semesterDb.insertSemesters(new.uniqueSubtract(old))
semesterDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
}
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =

View File

@ -1,13 +1,11 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.StudentInfoDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -15,7 +13,7 @@ import javax.inject.Singleton
@Singleton
class StudentInfoRepository @Inject constructor(
private val studentInfoDao: StudentInfoDao,
private val sdk: Sdk
private val wulkanowySdkFactory: WulkanowySdkFactory,
) {
private val saveFetchResultMutex = Mutex()
@ -30,16 +28,16 @@ class StudentInfoRepository @Inject constructor(
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
.getStudentInfo().mapToEntity(semester)
wulkanowySdkFactory.create(student, semester)
.getStudentInfo()
.mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(studentInfoDao) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
studentInfoDao.removeOldAndSaveNew(
oldItems = listOf(old),
newItems = listOf(new),
)
} else if (old == null) {
studentInfoDao.insertAll(listOf(new))
}

View File

@ -1,23 +1,25 @@
package io.github.wulkanowy.data.repositories
import androidx.room.withTransaction
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.security.Scrambler
import io.github.wulkanowy.utils.switchSemester
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -26,7 +28,7 @@ class StudentRepository @Inject constructor(
private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao,
private val semesterDb: SemesterDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val appDatabase: AppDatabase,
private val scrambler: Scrambler,
) {
@ -37,9 +39,10 @@ class StudentRepository @Inject constructor(
pin: String,
symbol: String,
token: String
): RegisterUser = sdk
): RegisterUser = wulkanowySdkFactory.create()
.getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null)
.also { it.logErrors() }
suspend fun getUserSubjectsFromScrapper(
email: String,
@ -47,18 +50,20 @@ class StudentRepository @Inject constructor(
scrapperBaseUrl: String,
domainSuffix: String,
symbol: String
): RegisterUser = sdk
): RegisterUser = wulkanowySdkFactory.create()
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getStudentsHybrid(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): RegisterUser = sdk
): RegisterUser = wulkanowySdkFactory.create()
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
@ -100,6 +105,46 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun updateCurrentStudentAuthStatus() {
Timber.i("Check isAuthorized: started")
val student = getCurrentStudent()
if (student.isAuthorized) {
Timber.i("Check isAuthorized: already authorized")
return
}
val initializedSdk = wulkanowySdkFactory.create(student)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Check isAuthorized: error occurred") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.d("Check isAuthorized: current user is null")
return
}
val currentStudentSemesters = semesterDb.loadAll(student.studentId, student.classId)
if (currentStudentSemesters.isEmpty()) {
Timber.d("Check isAuthorized: apply empty semesters workaround")
semesterDb.insertSemesters(
items = newCurrentStudent.semesters.mapToEntities(student.studentId),
)
}
if (!newCurrentStudent.isAuthorized) {
Timber.i("Check isAuthorized: authorization required")
throw NoAuthorizationException()
}
val studentIsAuthorized = StudentIsAuthorized(
id = student.id,
isAuthorized = true
)
Timber.i("Check isAuthorized: already authorized, update local status")
studentDb.update(studentIsAuthorized)
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
@ -149,20 +194,24 @@ class StudentRepository @Inject constructor(
.distinctBy { it.student.studentName }.size == 1
suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) =
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.authorizePermission(pesel)
suspend fun refreshStudentName(student: Student, semester: Semester) {
val newCurrentApiStudent = sdk.init(student)
.switchSemester(semester)
.getCurrentStudent() ?: return
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
.getOrNull() ?: return
val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id }
studentDb.update(studentName)
semesterDb.removeOldAndSaveNew(
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
)
}
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
@ -175,4 +224,18 @@ class StudentRepository @Inject constructor(
appDatabase.clearAllTables()
}
}
private fun RegisterUser.logErrors() {
val symbolsErrors = symbols.filter { it.error != null }
.map { it.error }
val unitsErrors = symbols.flatMap { it.schools }
.filter { it.error != null }
.map { it.error }
(symbolsErrors + unitsErrors).forEach { error ->
Timber.e(error, "Error occurred while fetching students")
}
}
}
class NoAuthorizationException : Exception()

View File

@ -1,15 +1,13 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@ -18,7 +16,7 @@ import javax.inject.Singleton
@Singleton
class SubjectRepository @Inject constructor(
private val subjectDao: SubjectDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -39,15 +37,15 @@ class SubjectRepository @Inject constructor(
},
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getSubjects()
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
subjectDao.deleteAll(old uniqueSubtract new)
subjectDao.insertAll(new uniqueSubtract old)
subjectDao.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)

View File

@ -1,15 +1,13 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
@ -18,7 +16,7 @@ import javax.inject.Singleton
@Singleton
class TeacherRepository @Inject constructor(
private val teacherDb: TeacherDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
) {
@ -39,15 +37,15 @@ class TeacherRepository @Inject constructor(
},
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = {
sdk.init(student)
.switchSemester(semester)
wulkanowySdkFactory.create(student, semester)
.getTeachers()
.mapToEntities(semester)
},
saveFetchResult = { old, new ->
teacherDb.deleteAll(old uniqueSubtract new)
teacherDb.insertAll(new uniqueSubtract old)
teacherDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)

View File

@ -1,15 +1,23 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
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.networkBoundResource
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.sync.Mutex
@ -23,7 +31,7 @@ class TimetableRepository @Inject constructor(
private val timetableDb: TimetableDao,
private val timetableAdditionalDb: TimetableAdditionalDao,
private val timetableHeaderDb: TimetableHeaderDao,
private val sdk: Sdk,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val schedulerHelper: TimetableNotificationSchedulerHelper,
private val refreshHelper: AutoRefreshHelper,
) {
@ -64,8 +72,7 @@ class TimetableRepository @Inject constructor(
},
query = { getFullTimetableFromDatabase(student, semester, start, end) },
fetch = {
val timetableFull = sdk.init(student)
.switchSemester(semester)
val timetableFull = wulkanowySdkFactory.create(student, semester)
.getTimetable(start.monday, end.sunday)
timetableFull.mapToEntities(semester)
@ -121,12 +128,12 @@ class TimetableRepository @Inject constructor(
}
}
fun getTimetableFromDatabase(
suspend fun getTimetableFromDatabase(
semester: Semester,
from: LocalDate,
start: LocalDate,
end: LocalDate
): Flow<List<Timetable>> {
return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end)
): List<Timetable> {
return timetableDb.load(semester.diaryId, semester.studentId, start, end)
}
suspend fun updateTimetable(timetable: List<Timetable>) {
@ -144,8 +151,10 @@ class TimetableRepository @Inject constructor(
new.apply { if (notify) isNotified = false }
}
timetableDb.deleteAll(lessonsToRemove)
timetableDb.insertAll(lessonsToAdd)
timetableDb.removeOldAndSaveNew(
oldItems = lessonsToRemove,
newItems = lessonsToAdd,
)
schedulerHelper.cancelScheduled(lessonsToRemove, student)
schedulerHelper.scheduleNotifications(lessonsToAdd, student)
@ -156,13 +165,17 @@ class TimetableRepository @Inject constructor(
new: List<TimetableAdditional>
) {
val oldFiltered = old.filter { !it.isAddedByUser }
timetableAdditionalDb.deleteAll(oldFiltered uniqueSubtract new)
timetableAdditionalDb.insertAll(new uniqueSubtract old)
timetableAdditionalDb.removeOldAndSaveNew(
oldItems = oldFiltered uniqueSubtract new,
newItems = new uniqueSubtract old,
)
}
private suspend fun refreshDayHeaders(old: List<TimetableHeader>, new: List<TimetableHeader>) {
timetableHeaderDb.deleteAll(old uniqueSubtract new)
timetableHeaderDb.insertAll(new uniqueSubtract old)
timetableHeaderDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
}
fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant {

View File

@ -1,10 +1,7 @@
package io.github.wulkanowy.domain.timetable
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import java.time.LocalDate
@ -16,18 +13,14 @@ class IsStudentHasLessonsOnWeekendUseCase @Inject constructor(
) {
suspend operator fun invoke(
student: Student,
semester: Semester,
currentDate: LocalDate = LocalDate.now(),
): Boolean {
val lessons = timetableRepository.getTimetable(
student = student,
val lessons = timetableRepository.getTimetableFromDatabase(
semester = semester,
start = currentDate.monday,
end = currentDate.sunday,
forceRefresh = false,
timetableType = TimetableRepository.TimetableType.NORMAL
).toFirstResult().dataOrNull?.lessons.orEmpty()
)
return isWeekendHasLessonsUseCase(lessons)
}
}

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.DispatchersProvider
@ -48,6 +49,7 @@ class SyncWorker @AssistedInject constructor(
val semester = semesterRepository.getCurrentSemester(student, true)
student to semester
} catch (e: Throwable) {
Timber.e(e)
return@withContext getResultFromErrors(listOf(e))
}
@ -59,7 +61,7 @@ class SyncWorker @AssistedInject constructor(
null
} catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
if (e is FeatureDisabledException || e is FeatureNotAvailableException || e is FeatureUnavailableException) {
null
} else {
Timber.e(e)

View File

@ -6,7 +6,6 @@ import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now
import javax.inject.Inject
@ -31,10 +30,9 @@ class TimetableWork @Inject constructor(
timetableRepository.getTimetableFromDatabase(
semester = semester,
from = startDate,
start = startDate,
end = endDate,
)
.first()
.filterNot { it.isNotified }
.let {
if (it.isNotEmpty()) changeTimetableNotification.notify(it, student)

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.base
import android.app.ActivityManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
@ -17,6 +18,8 @@ import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.openInternetBrowser
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
@ -36,16 +39,26 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
abstract var presenter: T
private var lastDialogOpenTime = mutableMapOf<String, Instant>()
override fun onCreate(savedInstanceState: Bundle?) {
inject()
themeManager.applyActivityTheme(this)
super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
applyCustomTaskDescription()
}
@Suppress("DEPRECATION")
setTaskDescription(
ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))
)
@Suppress("DEPRECATION")
private fun applyCustomTaskDescription() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) return
try {
val newColor = getThemeAttrColor(R.attr.colorSurface)
val taskDescription = ActivityManager.TaskDescription(null, null, newColor)
setTaskDescription(taskDescription)
} catch (e: Exception) {
Timber.e(e)
}
}
override fun showError(text: String, error: Throwable) {
@ -70,6 +83,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
}
override fun showExpiredCredentialsDialog() {
if (!shouldShowDialog(DIALOG_ERROR_BAD_CREDENTIALS)) return
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_expired_credentials_title)
.setMessage(R.string.main_expired_credentials_description)
@ -83,6 +98,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
}
override fun showDecryptionFailedDialog() {
if (!shouldShowDialog(DIALOG_ERROR_DECRYPTION_FAILED)) return
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin)
@ -119,4 +136,21 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
protected open fun inject() {
throw UnsupportedOperationException()
}
private fun shouldShowDialog(name: String): Boolean {
val lastOpenTime = lastDialogOpenTime[name]
val now = Instant.now()
if (lastOpenTime != null && now.isBefore(lastOpenTime.plusSeconds(1))) {
Timber.i("Dialog $name was shown less than a second ago. Skip")
return false
}
lastDialogOpenTime[name] = Instant.now()
return true
}
companion object {
private const val DIALOG_ERROR_BAD_CREDENTIALS = "dialog_error_bad_credentials"
private const val DIALOG_ERROR_DECRYPTION_FAILED = "dialog_error_decryption_failed"
}
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.base
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
import io.github.wulkanowy.data.repositories.NoAuthorizationException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -34,17 +34,21 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
}
protected open fun proceed(error: Throwable) {
showErrorMessage(context.resources.getErrorString(error), error)
showDefaultMessage(error)
when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException -> onDecryptionFailed()
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired()
is NoAuthorizationException -> onAuthorizationRequired()
is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl)
}
}
fun showDefaultMessage(error: Throwable) {
showErrorMessage(context.resources.getErrorString(error), error)
}
open fun clear() {
showErrorMessage = { _, _ -> }
onExpiredCredentials = {}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance
import android.content.res.ColorStateList
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
@ -33,17 +34,17 @@ class AttendanceAdapter @Inject constructor() :
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val context = holder.binding.root.context
val item = items[position]
with(holder.binding) {
attendanceItemNumber.text = item.number.toString()
attendanceItemSubject.text = item.subject.ifBlank {
root.context.getString(R.string.all_no_data)
}
attendanceItemSubject.text = item.subject
.ifBlank { context.getString(R.string.all_no_data) }
attendanceItemDescription.setText(item.descriptionRes)
attendanceItemDescription.setTextColor(
root.context.getThemeAttrColor(
context.getThemeAttrColor(
when {
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
@ -61,13 +62,15 @@ class AttendanceAdapter @Inject constructor() :
attendanceItemAlert.isVisible =
item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) }
attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor(
when{
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
else -> android.R.attr.colorPrimary
}
))
attendanceItemAlert.imageTintList = ColorStateList.valueOf(
context.getThemeAttrColor(
when {
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
else -> android.R.attr.colorPrimary
}
)
)
attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseInfo.visibility = View.GONE
attendanceItemExcuseCheckbox.visibility = View.GONE

View File

@ -4,18 +4,14 @@ import android.annotation.SuppressLint
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.time.DayOfWeek
@ -210,7 +206,7 @@ class AttendancePresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester)
checkInitialAndCurrentDate(semester)
attendanceRepository.getAttendance(
student = student,
semester = semester,
@ -266,15 +262,13 @@ class AttendancePresenter @Inject constructor(
.launch()
}
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
private suspend fun checkInitialAndCurrentDate(semester: Semester) {
if (initialDate == null) {
val lessons = attendanceRepository.getAttendance(
student = student,
val lessons = attendanceRepository.getAttendanceFromDatabase(
semester = semester,
start = now().monday,
end = now().sunday,
forceRefresh = false,
).toFirstResult().dataOrNull.orEmpty()
).firstOrNull().orEmpty()
isWeekendHasLessons = isWeekendHasLessons(lessons)
initialDate = getInitialDate(semester)
}
@ -316,6 +310,7 @@ class AttendancePresenter @Inject constructor(
showContent(false)
showExcuseButton(false)
}
is Resource.Success -> {
Timber.i("Excusing for absence result: Success")
analytics.logEvent("excuse_absence", "items" to attendanceToExcuseList.size)
@ -328,6 +323,7 @@ class AttendancePresenter @Inject constructor(
}
loadData(forceRefresh = true)
}
is Resource.Error -> {
Timber.i("Excusing for absence result: An exception occurred")
errorHandler.dispatch(it.error)

View File

@ -78,4 +78,9 @@ class AuthDialog : BaseDialogFragment<DialogAuthBinding>(), AuthView {
override fun showDescriptionWithName(name: String) {
binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml()
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class AuthPresenter @Inject constructor(
@ -57,13 +58,19 @@ class AuthPresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
Timber.d("Auth succeed: $isSuccess")
if (isSuccess) {
studentRepository.refreshStudentName(student, semester)
studentRepository.refreshStudentAfterAuthorize(student, semester)
}
isSuccess
}
.onFailure { errorHandler.dispatch(it) }
.onFailure {
errorHandler.dispatch(it)
view?.showProgress(false)
view?.showContent(true)
}
.onSuccess {
Timber.d("Auth fully succeed: $it")
if (it) {
view?.showSuccess(true)
view?.showContent(false)

View File

@ -10,9 +10,10 @@ import android.webkit.WebViewClient
import androidx.core.os.bundleOf
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.databinding.DialogCaptchaBinding
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import timber.log.Timber
import javax.inject.Inject
@ -20,7 +21,10 @@ import javax.inject.Inject
class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
@Inject
lateinit var sdk: Sdk
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
@Inject
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
private var webView: WebView? = null
@ -55,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = sdk.userAgent
userAgentString = wulkanowySdkFactory.create().userAgent
}
webViewClient = object : WebViewClient() {
@ -80,6 +84,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
}
override fun onDestroy() {
webkitCookieManagerProxy.webkitCookieManager?.flush()
webView?.destroy()
super.onDestroy()
}

View File

@ -304,6 +304,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh
)
}
.mapResourceData { it.map { messageWithAuthor -> messageWithAuthor.message } }
.onResourceError { errorHandler.dispatch(it) }
.takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
@ -438,7 +439,7 @@ class DashboardPresenter @Inject constructor(
private fun loadLessons(student: Student, forceRefresh: Boolean) {
flatResourceFlow {
val semester = semesterRepository.getCurrentSemester(student)
val date = when (isStudentHasLessonsOnWeekendUseCase(student, semester)) {
val date = when (isStudentHasLessonsOnWeekendUseCase(semester)) {
true -> LocalDate.now()
else -> LocalDate.now().nextOrSameSchoolDay
}

View File

@ -159,7 +159,7 @@ class GradeAverageProvider @Inject constructor(
?.updateModifiers(student, config).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
config.isOptionalArithmeticAverage
isOptionalArithmeticAverage = config.isOptionalArithmeticAverage,
)
} else {
secondSemesterSubject.average
@ -173,13 +173,21 @@ class GradeAverageProvider @Inject constructor(
config: AverageCalcParams,
): Double {
return if (!isAnyVulcanAverage || config.forceAverageCalc) {
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
val isSecondSemesterHasWeightGrade = secondSemesterSubject.grades
.any { it.weightValue > .0 }
val isSecondSemesterHasArithmeticGrade = secondSemesterSubject.grades
.all { it.weightValue == .0 } && config.isOptionalArithmeticAverage
val isSecondSemesterHaveAverage =
isSecondSemesterHasWeightGrade || isSecondSemesterHasArithmeticGrade
val divider = if (isSecondSemesterHaveAverage) 2 else 1
val secondSemesterAverage = secondSemesterSubject.grades
.updateModifiers(student, config)
.calcAverage(config.isOptionalArithmeticAverage)
.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
val firstSemesterAverage = firstSemesterSubject?.grades
?.updateModifiers(student, config)
?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage
?.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
?: secondSemesterAverage
(secondSemesterAverage + firstSemesterAverage) / divider
} else {
@ -225,7 +233,7 @@ class GradeAverageProvider @Inject constructor(
subject = summary.subject,
average = if (!isAnyAverage || params.forceAverageCalc) {
grades.updateModifiers(student, params)
.calcAverage(params.isOptionalArithmeticAverage)
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
} else summary.average,
points = summary.pointsSum,
summary = summary,
@ -286,8 +294,13 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = if (calcAverage) details.updateModifiers(student, params)
.calcAverage(params.isOptionalArithmeticAverage) else .0
average = when {
calcAverage -> details
.updateModifiers(student, params)
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
else -> .0
}
)
}
}

View File

@ -7,7 +7,6 @@ import android.view.MenuItem
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
@ -31,14 +30,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
@Inject
lateinit var presenter: GradePresenter
private val pagerAdapter by lazy {
BaseFragmentPagerAdapter(
fragmentManager = childFragmentManager,
pagesCount = 3,
lifecycle = lifecycle,
)
}
private var semesterSwitchMenu: MenuItem? = null
companion object {
@ -52,6 +43,8 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem
private var pagerAdapter: BaseFragmentPagerAdapter? = null
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -71,13 +64,26 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun initView() {
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun initTabs(pageCount: Int) {
pagerAdapter = BaseFragmentPagerAdapter(
lifecycle = lifecycle,
pagesCount = pageCount,
fragmentManager = childFragmentManager
)
with(binding.gradeViewPager) {
adapter = pagerAdapter
offscreenPageLimit = 3
setOnSelectPageListener(presenter::onPageSelected)
}
with(pagerAdapter) {
with(pagerAdapter!!) {
containerId = binding.gradeViewPager.id
titleFactory = {
when (it) {
@ -99,11 +105,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
binding.gradeTabLayout.elevation = requireContext().dpToPx(4f)
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -169,19 +170,20 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)
?.onParentLoadData(semesterId, forceRefresh)
}
override fun notifyChildParentReselected(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
}
override fun notifyChildSemesterChange(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
}
override fun onDestroyView() {
pagerAdapter = null
presenter.onDetachView()
super.onDestroyView()
}

View File

@ -22,11 +22,8 @@ class GradePresenter @Inject constructor(
) : BasePresenter<GradeView>(errorHandler, studentRepository) {
private var selectedIndex = 0
private var schoolYear = 0
private var semesters = emptyList<Semester>()
private var availableSemesters = emptyList<Semester>()
private val loadedSemesterId = mutableMapOf<Int, Int>()
private lateinit var lastError: Throwable
@ -40,7 +37,7 @@ class GradePresenter @Inject constructor(
}
fun onCreateMenu() {
if (semesters.isEmpty()) view?.showSemesterSwitch(false)
if (availableSemesters.isEmpty()) view?.showSemesterSwitch(false)
}
fun onViewReselected() {
@ -49,8 +46,8 @@ class GradePresenter @Inject constructor(
}
fun onSemesterSwitch(): Boolean {
if (semesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, semesters.take(2))
if (availableSemesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, availableSemesters.take(2))
}
return true
}
@ -83,7 +80,7 @@ class GradePresenter @Inject constructor(
}
fun onPageSelected(index: Int) {
if (semesters.isNotEmpty()) loadChild(index)
if (availableSemesters.isNotEmpty()) loadChild(index)
}
fun onRetry() {
@ -101,16 +98,24 @@ class GradePresenter @Inject constructor(
private fun loadData() {
resourceFlow {
val student = studentRepository.getCurrentStudent()
semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
val semesters = semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
student to semesters
}
.logResourceStatus("load grade data")
.onResourceData {
val current = it.getCurrentOrLast()
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
schoolYear = current.schoolYear
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
view?.setCurrentSemesterName(current.semesterName, schoolYear)
.onResourceData { (student, semesters) ->
val currentSemester = semesters.getCurrentOrLast()
selectedIndex =
if (selectedIndex == 0) currentSemester.semesterName else selectedIndex
schoolYear = currentSemester.schoolYear
availableSemesters = semesters.filter { semester ->
semester.diaryId == currentSemester.diaryId
}
view?.run {
initTabs(if (student.isEduOne == true) 2 else 3)
setCurrentSemesterName(currentSemester.semesterName, schoolYear)
Timber.i("Loading grade data: Attempt load index $currentPageIndex")
loadChild(currentPageIndex)
showErrorView(false)
@ -131,10 +136,10 @@ class GradePresenter @Inject constructor(
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${availableSemesters.joinToString { it.semesterName.toString() }}")
val newSelectedSemesterId = try {
semesters.first { it.semesterName == selectedIndex }.semesterId
availableSemesters.first { it.semesterName == selectedIndex }.semesterId
} catch (e: NoSuchElementException) {
Timber.e(e, "Selected semester no exists")
return

View File

@ -9,6 +9,8 @@ interface GradeView : BaseView {
fun initView()
fun initTabs(pageCount: Int)
fun showContent(show: Boolean)
fun showProgress(show: Boolean)

View File

@ -98,7 +98,9 @@ class HomeworkAddDialog : BaseDialogFragment<DialogHomeworkAddBinding>(), Homewo
rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear,
onDateSelected = {
date = it
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
if (isAdded) {
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
}
}
)
}

View File

@ -94,6 +94,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() }
loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() }
loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() }
loginFormDomainSuffix.doOnTextChanged { _, _, _, _ -> presenter.onDomainSuffixChanged() }
loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() }
loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() }
@ -188,6 +189,12 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
}
}
override fun setDomainSuffixInvalid() {
with(binding.loginFormDomainSuffixLayout) {
error = getString(R.string.login_invalid_domain_suffix)
}
}
override fun clearUsernameError() {
binding.loginFormUsernameLayout.error = null
binding.loginFormErrorBox.isVisible = false
@ -206,6 +213,10 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding.loginFormErrorBox.isVisible = false
}
override fun clearDomainSuffixError() {
binding.loginFormDomainSuffixLayout.error = null
}
override fun showSoftKeyboard() {
activity?.showSoftInput()
}

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -101,6 +102,12 @@ class LoginFormPresenter @Inject constructor(
}
}
fun onDomainSuffixChanged() {
view?.apply {
clearDomainSuffixError()
}
}
fun updateCustomDomainSuffixVisibility() {
view?.run {
showDomainSuffixInput("customSuffix" in formHostValue)
@ -159,7 +166,7 @@ class LoginFormPresenter @Inject constructor(
fun onSignInClick() {
val loginData = getLoginData()
if (!validateCredentials(loginData.login, loginData.password, loginData.baseUrl)) return
if (!validateCredentials(loginData)) return
resourceFlow {
studentRepository.getUserSubjectsFromScrapper(
@ -198,6 +205,9 @@ class LoginFormPresenter @Inject constructor(
}
.onResourceError {
loginErrorHandler.dispatch(it)
if (it is InvalidSymbolException) {
loginErrorHandler.showDefaultMessage(it)
}
lastError = it
view?.showContact(true)
analytics.logEvent(
@ -229,24 +239,29 @@ class LoginFormPresenter @Inject constructor(
view?.onRecoverClick()
}
private fun validateCredentials(login: String, password: String, host: String): Boolean {
private fun validateCredentials(loginData: LoginData): Boolean {
var isCorrect = true
if (login.isEmpty()) {
if (loginData.login.isEmpty()) {
view?.setErrorUsernameRequired()
isCorrect = false
} else {
if ("@" in login && "login" in host) {
if ("@" in loginData.login && "login" in loginData.baseUrl) {
view?.setErrorLoginRequired()
isCorrect = false
}
if ("@" !in login && "email" in host) {
if ("@" !in loginData.login && "email" in loginData.baseUrl) {
view?.setErrorEmailRequired()
isCorrect = false
}
if ("@" in login && "||" !in login && "login" !in host && "email" !in host) {
val emailHost = login.substringAfter("@")
val emailDomain = URL(host).host
val isEmailLogin = "@" in loginData.login
val isEmailWithLogin = "||" !in loginData.login
val isLoginNotRequired = "login" !in loginData.baseUrl
val isEmailNotRequired = "email" !in loginData.baseUrl
if (isEmailLogin && isEmailWithLogin && isLoginNotRequired && isEmailNotRequired) {
val emailHost = loginData.login.substringAfter("@")
val emailDomain = URL(loginData.baseUrl).host
if (!emailHost.equals(emailDomain, true)) {
view?.setErrorEmailInvalid(domain = emailDomain)
isCorrect = false
@ -254,16 +269,21 @@ class LoginFormPresenter @Inject constructor(
}
}
if (password.isEmpty()) {
if (loginData.password.isEmpty()) {
view?.setErrorPassRequired(focus = isCorrect)
isCorrect = false
}
if (password.length < 6 && password.isNotEmpty()) {
if (loginData.password.length < 6 && loginData.password.isNotEmpty()) {
view?.setErrorPassInvalid(focus = isCorrect)
isCorrect = false
}
if (loginData.domainSuffix !in listOf("", "rc", "kurs")) {
view?.setDomainSuffixInvalid()
isCorrect = false
}
return isCorrect
}
}

View File

@ -46,12 +46,16 @@ interface LoginFormView : BaseView {
fun setErrorEmailInvalid(domain: String)
fun setDomainSuffixInvalid()
fun clearUsernameError()
fun clearPassError()
fun clearHostError()
fun clearDomainSuffixError()
fun showSoftKeyboard()
fun hideSoftKeyboard()

View File

@ -96,10 +96,7 @@ class LoginSymbolPresenter @Inject constructor(
?.takeIf { it.symbol == loginData.userEnteredSymbol }
if (enteredSymbolDetails?.error is InvalidSymbolException) {
view?.run {
setErrorSymbolInvalid()
showContact(true)
}
showInvalidSymbolError()
} else {
Timber.i("Login with symbol result: Success")
view?.navigateToStudentSelect(loginData, requireNotNull(user.data))
@ -128,6 +125,9 @@ class LoginSymbolPresenter @Inject constructor(
loginErrorHandler.dispatch(user.error)
lastError = user.error
view?.showContact(true)
if (user.error is InvalidSymbolException) {
showInvalidSymbolError()
}
}
}
}.onResourceNotLoading {
@ -145,6 +145,13 @@ class LoginSymbolPresenter @Inject constructor(
return normalizedSymbol in definitelyInvalidSymbols
}
private fun showInvalidSymbolError() {
view?.run {
setErrorSymbolInvalid()
showContact(true)
}
}
fun onFaqClick() {
view?.openFaqPage()
}

View File

@ -11,10 +11,8 @@ import android.view.View
import android.widget.RemoteViews
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.dataOrThrow
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.toFirstResult
@ -69,8 +67,7 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
appWidgetIds?.forEach { widgetId ->
val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0)
val luckyNumberResource = getLuckyNumber(studentId, widgetId)
val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber?.toString()
val luckyNumber = getLuckyNumber(studentId, widgetId)?.luckyNumber?.toString()
val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber)
.apply {
setTextViewText(R.id.luckyNumberWidgetValue, luckyNumber ?: "-")
@ -143,18 +140,18 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id)
}
}
else -> null
}
if (currentStudent != null) {
luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false)
.toFirstResult()
} else {
Resource.Success<LuckyNumber?>(null)
}
.dataOrThrow
} else null
} catch (e: Exception) {
Timber.e(e, "An error has occurred in lucky number provider")
Resource.Error(e)
null
}
}

View File

@ -73,6 +73,7 @@ class MainPresenter @Inject constructor(
syncManager.startPeriodicSyncWorker()
checkAppSupport()
updateCurrentStudentAuthStatus()
analytics.logEvent("app_open", "destination" to initDestination.toString())
Timber.i("Main view was initialized with $initDestination")
@ -191,4 +192,11 @@ class MainPresenter @Inject constructor(
view?.showStudentAvatar(currentStudent)
}
private fun updateCurrentStudentAuthStatus() {
presenterScope.launch {
runCatching { studentRepository.updateCurrentStudentAuthStatus() }
.onFailure { errorHandler.dispatch(it) }
}
}
}

View File

@ -47,7 +47,6 @@ class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(),
}
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(

View File

@ -50,12 +50,15 @@ class MessagePreviewAdapter @Inject constructor() :
ViewType.MESSAGE.id -> MessageViewHolder(
ItemMessagePreviewBinding.inflate(inflater, parent, false)
)
ViewType.DIVIDER.id -> DividerViewHolder(
ItemMessageDividerBinding.inflate(inflater, parent, false)
)
ViewType.ATTACHMENT.id -> AttachmentViewHolder(
ItemMessageAttachmentBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
@ -66,6 +69,7 @@ class MessagePreviewAdapter @Inject constructor() :
holder,
requireNotNull(messageWithAttachment).message
)
is AttachmentViewHolder -> bindAttachment(
holder,
requireNotNull(messageWithAttachment).attachments[position - 2]
@ -82,9 +86,11 @@ class MessagePreviewAdapter @Inject constructor() :
recipientCount > 1 -> {
context.getString(R.string.message_read_by, message.readBy, recipientCount)
}
message.readBy == 1 || (isReceived && !message.unread) -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes))
}
else -> context.getString(R.string.message_read, context.getString(R.string.all_no))
}

View File

@ -44,18 +44,33 @@ class MessagePreviewFragment :
private var menuForwardButton: MenuItem? = null
private var menuRestoreButton: MenuItem? = null
private var menuDeleteButton: MenuItem? = null
private var menuDeleteForeverButton: MenuItem? = null
private var menuShareButton: MenuItem? = null
private var menuPrintButton: MenuItem? = null
private var menuMuteButton: MenuItem? = null
override val titleStringId: Int
get() = R.string.message_title
override val deleteMessageSuccessString: String
get() = getString(R.string.message_delete_success)
override val muteMessageSuccessString: String
get() = getString(R.string.message_mute_success)
override val unmuteMessageSuccessString: String
get() = getString(R.string.message_unmute_success)
override val restoreMessageSuccessString: String
get() = getString(R.string.message_restore_success)
override val messageNoSubjectString: String
get() = getString(R.string.message_no_subject)
@ -67,10 +82,10 @@ class MessagePreviewFragment :
get() = getString(R.string.message_not_exists)
companion object {
const val MESSAGE_ID_KEY = "message_id"
private const val MESSAGE_ARG_KEY = "message"
fun newInstance(message: Message) = MessagePreviewFragment().apply {
arguments = bundleOf(MESSAGE_ID_KEY to message)
arguments = bundleOf(MESSAGE_ARG_KEY to message)
}
}
@ -86,7 +101,7 @@ class MessagePreviewFragment :
messageContainer = binding.messagePreviewContainer
presenter.onAttachView(
view = this,
message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY),
message = requireArguments().serializable(MESSAGE_ARG_KEY),
)
}
@ -103,9 +118,12 @@ class MessagePreviewFragment :
inflater.inflate(R.menu.action_menu_message_preview, menu)
menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply)
menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward)
menuRestoreButton = menu.findItem(R.id.messagePreviewMenuRestore)
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
menuDeleteForeverButton = menu.findItem(R.id.messagePreviewMenuDeleteForever)
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
menuMuteButton = menu.findItem(R.id.messagePreviewMenuMute)
presenter.onCreateOptionsMenu()
menu.findItem(R.id.mainMenuAccount).isVisible = false
@ -115,9 +133,12 @@ class MessagePreviewFragment :
return when (item.itemId) {
R.id.messagePreviewMenuReply -> presenter.onReply()
R.id.messagePreviewMenuForward -> presenter.onForward()
R.id.messagePreviewMenuRestore -> presenter.onMessageRestore()
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
R.id.messagePreviewMenuDeleteForever -> presenter.onMessageDelete()
R.id.messagePreviewMenuShare -> presenter.onShare()
R.id.messagePreviewMenuPrint -> presenter.onPrint()
R.id.messagePreviewMenuMute -> presenter.onMute()
else -> false
}
}
@ -129,6 +150,11 @@ class MessagePreviewFragment :
}
}
override fun updateMuteToggleButton(isMuted: Boolean) {
menuMuteButton?.setTitle(if (isMuted) R.string.message_unmute else R.string.message_mute)
}
override fun showProgress(show: Boolean) {
binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE
}
@ -137,20 +163,15 @@ class MessagePreviewFragment :
binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showOptions(show: Boolean, isReplayable: Boolean) {
menuReplyButton?.isVisible = isReplayable
override fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean) {
menuReplyButton?.isVisible = show && isReplayable
menuForwardButton?.isVisible = show
menuDeleteButton?.isVisible = show
menuRestoreButton?.isVisible = show && isRestorable
menuDeleteButton?.isVisible = show && !isRestorable
menuDeleteForeverButton?.isVisible = show && isRestorable
menuShareButton?.isVisible = show
menuPrintButton?.isVisible = show
}
override fun setDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_delete_forever)
}
override fun setNotDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_move_to_trash)
menuMuteButton?.isVisible = show && isReplayable
}
override fun showErrorView(show: Boolean) {
@ -212,11 +233,6 @@ class MessagePreviewFragment :
(activity as MainActivity).popView()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(MESSAGE_ID_KEY, presenter.message)
super.onSaveInstanceState(outState)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -3,10 +3,15 @@ package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import androidx.core.text.parseAsHtml
import io.github.wulkanowy.R
import io.github.wulkanowy.data.*
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.enums.MessageFolder
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@ -14,9 +19,11 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler,
@ -26,20 +33,17 @@ class MessagePreviewPresenter @Inject constructor(
private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
var message: Message? = null
var attachments: List<MessageAttachment>? = null
private var messageWithAttachments: MessageWithAttachment? = null
private lateinit var lastError: Throwable
private var retryCallback: () -> Unit = {}
fun onAttachView(view: MessagePreviewView, message: Message?) {
fun onAttachView(view: MessagePreviewView, message: Message) {
super.onAttachView(view)
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError
this.message = message
loadData(requireNotNull(message))
loadData(message)
}
private fun onMessageLoadRetry(message: Message) {
@ -66,25 +70,24 @@ class MessagePreviewPresenter @Inject constructor(
.logResourceStatus("message ${messageToLoad.messageId} preview")
.onResourceData {
if (it != null) {
message = it.message
attachments = it.attachments
messageWithAttachments = it
view?.apply {
setMessageWithAttachment(it)
showContent(true)
initOptions()
updateMuteToggleButton(isMuted = it.mutedMessageSender != null)
if (preferencesRepository.isIncognitoMode && it.message.unread) {
showMessage(R.string.message_incognito_description)
}
}
} else {
delay(1.seconds)
view?.run {
showMessage(messageNotExists)
popView()
}
}
}
.onResourceSuccess {
}.onResourceSuccess {
if (it != null) {
analytics.logEvent(
"load_item",
@ -92,31 +95,28 @@ class MessagePreviewPresenter @Inject constructor(
"length" to it.message.content.length
)
}
}
.onResourceNotLoading { view?.showProgress(false) }
.onResourceError {
}.onResourceNotLoading { view?.showProgress(false) }.onResourceError {
retryCallback = { onMessageLoadRetry(messageToLoad) }
errorHandler.dispatch(it)
}
.launch()
}.launch()
}
fun onReply(): Boolean {
return if (message != null) {
view?.openMessageReply(message)
return if (messageWithAttachments?.message != null) {
view?.openMessageReply(messageWithAttachments?.message)
true
} else false
}
fun onForward(): Boolean {
return if (message != null) {
view?.openMessageForward(message)
return if (messageWithAttachments?.message != null) {
view?.openMessageForward(messageWithAttachments?.message)
true
} else false
}
fun onShare(): Boolean {
val message = message ?: return false
val message = messageWithAttachments?.message ?: return false
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val text = buildString {
@ -129,13 +129,15 @@ class MessagePreviewPresenter @Inject constructor(
appendLine(message.content.parseAsHtml())
if (!attachments.isNullOrEmpty()) {
if (!messageWithAttachments?.attachments.isNullOrEmpty()) {
appendLine()
appendLine("Załączniki:")
append(attachments.orEmpty().joinToString(separator = "\n") { attachment ->
"${attachment.filename}: ${attachment.url}"
})
append(
messageWithAttachments?.attachments.orEmpty()
.joinToString(separator = "\n") { attachment ->
"${attachment.filename}: ${attachment.url}"
})
}
}
@ -148,7 +150,7 @@ class MessagePreviewPresenter @Inject constructor(
@SuppressLint("NewApi")
fun onPrint(): Boolean {
val message = message ?: return false
val message = messageWithAttachments?.message ?: return false
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val dateString = message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")
@ -159,8 +161,7 @@ class MessagePreviewPresenter @Inject constructor(
append("<div><h4>Od</h4>${message.sender}</div>")
append("<div><h4>DO</h4>${message.recipients}</div>")
}
val messageContent = "<p>${message.content}</p>"
.replace(Regex("[\\n\\r]{2,}"), "</p><p>")
val messageContent = "<p>${message.content}</p>".replace(Regex("[\\n\\r]{2,}"), "</p><p>")
.replace(Regex("[\\n\\r]"), "<br>")
val jobName = buildString {
@ -171,9 +172,7 @@ class MessagePreviewPresenter @Inject constructor(
}
view?.apply {
val html = printHTML
.replace("%SUBJECT%", subject)
.replace("%CONTENT%", messageContent)
val html = printHTML.replace("%SUBJECT%", subject).replace("%CONTENT%", messageContent)
.replace("%INFO%", infoContent)
printDocument(html, jobName)
}
@ -181,34 +180,69 @@ class MessagePreviewPresenter @Inject constructor(
return true
}
private fun deleteMessage() {
message ?: return
private fun restoreMessage() {
val message = messageWithAttachments?.message ?: return
view?.run {
showContent(false)
showProgress(true)
showOptions(show = false, isReplayable = false)
showOptions(
show = false,
isReplayable = false,
isRestorable = false,
)
showErrorView(false)
}
Timber.i("Delete message ${message?.messageGlobalKey}")
Timber.i("Restore message ${message.messageGlobalKey}")
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox, message!!)
messageRepository.restoreMessages(student, mailbox, listOfNotNull(message))
}
.onFailure {
retryCallback = { onMessageDelete() }
retryCallback = { onMessageRestore() }
errorHandler.dispatch(it)
}
.onSuccess {
view?.run {
showMessage(deleteMessageSuccessString)
showMessage(restoreMessageSuccessString)
popView()
}
}
view?.showProgress(false)
}
}
private fun deleteMessage() {
messageWithAttachments?.message ?: return
view?.run {
showContent(false)
showProgress(true)
showOptions(
show = false,
isReplayable = false,
isRestorable = false,
)
showErrorView(false)
}
Timber.i("Delete message ${messageWithAttachments?.message?.messageGlobalKey}")
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
messageRepository.deleteMessage(student, messageWithAttachments?.message!!)
}.onFailure {
retryCallback = { onMessageDelete() }
errorHandler.dispatch(it)
}.onSuccess {
view?.run {
showMessage(deleteMessageSuccessString)
popView()
}
}
view?.showProgress(false)
}
@ -224,6 +258,11 @@ class MessagePreviewPresenter @Inject constructor(
}
}
fun onMessageRestore(): Boolean {
restoreMessage()
return true
}
fun onMessageDelete(): Boolean {
deleteMessage()
return true
@ -232,20 +271,39 @@ class MessagePreviewPresenter @Inject constructor(
private fun initOptions() {
view?.apply {
showOptions(
show = message != null,
isReplayable = message?.folderId != MessageFolder.SENT.id,
show = messageWithAttachments?.message != null,
isReplayable = messageWithAttachments?.message?.folderId == MessageFolder.RECEIVED.id,
isRestorable = messageWithAttachments?.message?.folderId == MessageFolder.TRASHED.id,
)
message?.let {
when (it.folderId == MessageFolder.TRASHED.id) {
true -> setDeletedOptionsLabels()
false -> setNotDeletedOptionsLabels()
}
}
}
}
fun onCreateOptionsMenu() {
initOptions()
}
fun onMute(): Boolean {
val message = messageWithAttachments?.message ?: return false
val isMuted = messageWithAttachments?.mutedMessageSender != null
presenterScope.launch {
runCatching {
when (isMuted) {
true -> {
messageRepository.unmuteMessage(message.correspondents)
view?.run { showMessage(unmuteMessageSuccessString) }
}
false -> {
messageRepository.muteMessage(message.correspondents)
view?.run { showMessage(muteMessageSuccessString) }
}
}
}.onFailure {
errorHandler.dispatch(it)
}
}
view?.updateMuteToggleButton(isMuted)
return true
}
}

View File

@ -9,6 +9,12 @@ interface MessagePreviewView : BaseView {
val deleteMessageSuccessString: String
val muteMessageSuccessString: String
val unmuteMessageSuccessString: String
val restoreMessageSuccessString: String
val messageNoSubjectString: String
val printHTML: String
@ -19,6 +25,8 @@ interface MessagePreviewView : BaseView {
fun setMessageWithAttachment(item: MessageWithAttachment)
fun updateMuteToggleButton(isMuted: Boolean)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
@ -29,11 +37,7 @@ interface MessagePreviewView : BaseView {
fun setErrorRetryCallback(callback: () -> Unit)
fun showOptions(show: Boolean, isReplayable: Boolean)
fun setDeletedOptionsLabels()
fun setNotDeletedOptionsLabels()
fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean)
fun openMessageReply(message: Message?)

View File

@ -203,7 +203,7 @@ class SendMessagePresenter @Inject constructor(
subject = subject,
content = content,
recipients = recipients,
mailboxId = mailbox.globalKey,
mailbox = mailbox,
)
}.logResourceStatus("sending message").onEach {
when (it) {

View File

@ -18,8 +18,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class MessageTabAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class MessageTabAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit
@ -52,10 +51,11 @@ class MessageTabAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (MessageItemViewType.values()[viewType]) {
return when (MessageItemViewType.entries[viewType]) {
MessageItemViewType.FILTERS -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false)
)
MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false)
)
@ -137,7 +137,12 @@ class MessageTabAdapter @Inject constructor() :
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor))
isVisible = message.hasAttachments
}
messageItemUnreadIndicator.isVisible = message.unread
messageItemUnreadIndicator.isVisible = message.unread || item.isMuted
when (item.isMuted) {
true -> messageItemUnreadIndicator.setImageResource(R.drawable.ic_notifications_off)
else -> messageItemUnreadIndicator.setImageResource(R.drawable.ic_circle_notification)
}
root.setOnClickListener {
holder.bindingAdapterPosition.let {
@ -165,8 +170,7 @@ class MessageTabAdapter @Inject constructor() :
RecyclerView.ViewHolder(binding.root)
private class MessageTabDiffUtil(
private val old: List<MessageTabDataItem>,
private val new: List<MessageTabDataItem>
private val old: List<MessageTabDataItem>, private val new: List<MessageTabDataItem>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size

View File

@ -6,6 +6,7 @@ sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
data class MessageItem(
val message: Message,
val isMuted: Boolean,
val isSelected: Boolean,
val isActionMode: Boolean
) : MessageTabDataItem(MessageItemViewType.MESSAGE)

View File

@ -5,7 +5,9 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.View.*
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.CompoundButton
import androidx.annotation.StringRes
import androidx.appcompat.view.ActionMode
@ -64,10 +66,12 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
if (presenter.folder == MessageFolder.TRASHED) {
val menuItem = menu.findItem(R.id.messageTabContextMenuDelete)
menuItem.setTitle(R.string.message_delete_forever)
}
val isTrashFolder = presenter.folder == MessageFolder.TRASHED
menu.findItem(R.id.messageTabContextMenuDelete).setVisible(!isTrashFolder)
menu.findItem(R.id.messageTabContextMenuDeleteForever).setVisible(isTrashFolder)
menu.findItem(R.id.messageTabContextMenuRestore).setVisible(isTrashFolder)
return presenter.onPrepareActionMode()
}
@ -79,6 +83,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean {
when (menu.itemId) {
R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete()
R.id.messageTabContextMenuRestore -> presenter.onActionModeSelectRestore()
R.id.messageTabContextMenuDeleteForever -> presenter.onActionModeSelectDelete()
R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll()
}
return true

View File

@ -4,6 +4,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@ -39,7 +40,7 @@ class MessageTabPresenter @Inject constructor(
private var mailboxes: List<Mailbox> = emptyList()
private var selectedMailbox: Mailbox? = null
private var messages = emptyList<Message>()
private var messages = emptyList<MessageWithMutedAuthor>()
private val searchChannel = Channel<String>()
@ -120,8 +121,27 @@ class MessageTabPresenter @Inject constructor(
return true
}
fun onActionModeSelectRestore() {
Timber.i("Restore ${messagesToDelete.size} messages")
val messageList = messagesToDelete.toList()
presenterScope.launch {
view?.run {
showProgress(true)
showContent(false)
showActionMode(false)
}
runCatching {
val student = studentRepository.getCurrentStudent(true)
messageRepository.restoreMessages(student, selectedMailbox, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessage(R.string.message_messages_restored) }
}
}
fun onActionModeSelectDelete() {
Timber.i("Delete ${messagesToDelete.size} messages)")
Timber.i("Delete ${messagesToDelete.size} messages")
val messageList = messagesToDelete.toList()
presenterScope.launch {
@ -133,7 +153,7 @@ class MessageTabPresenter @Inject constructor(
runCatching {
val student = studentRepository.getCurrentStudent(true)
messageRepository.deleteMessages(student, selectedMailbox, messageList)
messageRepository.deleteMessages(student, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessage(R.string.message_messages_deleted) }
@ -141,7 +161,7 @@ class MessageTabPresenter @Inject constructor(
}
fun onActionModeSelectCheckAll() {
val messagesToSelect = getFilteredData()
val messagesToSelect = getFilteredData().map { it.message }
val isAllSelected = messagesToDelete.containsAll(messagesToSelect)
if (isAllSelected) {
@ -188,7 +208,7 @@ class MessageTabPresenter @Inject constructor(
view?.showActionMode(false)
}
val filteredData = getFilteredData()
val filteredData = getFilteredData().map { it.message }
view?.run {
updateActionModeTitle(messagesToDelete.size)
@ -320,25 +340,31 @@ class MessageTabPresenter @Inject constructor(
}
}
private fun getFilteredData(): List<Message> {
private fun getFilteredData(): List<MessageWithMutedAuthor> {
if (lastSearchQuery.trim().isEmpty()) {
val sortedMessages = messages.sortedByDescending { it.date }
val sortedMessages = messages.sortedByDescending { it.message.date }
return when {
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
}
(onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
else -> sortedMessages
}
} else {
val sortedMessages = messages
.map { it to calculateMatchRatio(it, lastSearchQuery) }
.sortedWith(compareBy<Pair<Message, Int>> { -it.second }.thenByDescending { it.first.date })
.map { it to calculateMatchRatio(it.message, lastSearchQuery) }
.sortedWith(compareBy<Pair<MessageWithMutedAuthor, Int>> { -it.second }.thenByDescending { it.first.message.date })
.filter { it.second > 6000 }
.map { it.first }
return when {
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments }
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments }
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
}
(onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
else -> sortedMessages
}
}
@ -367,8 +393,9 @@ class MessageTabPresenter @Inject constructor(
addAll(data.map { message ->
MessageTabDataItem.MessageItem(
message = message,
isSelected = messagesToDelete.any { it.messageGlobalKey == message.messageGlobalKey },
message = message.message,
isMuted = message.mutedMessageSender != null,
isSelected = messagesToDelete.any { it.messageGlobalKey == message.message.messageGlobalKey },
isActionMode = isActionMode
)
})

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
@ -29,6 +30,10 @@ class SchoolAnnouncementAdapter @Inject constructor() :
schoolAnnouncementItemDate.text = item.date.toFormattedString()
schoolAnnouncementItemType.text = item.subject
schoolAnnouncementItemContent.text = item.content.parseUonetHtml()
with(schoolAnnouncementItemAuthor) {
text = item.author
isVisible = !item.author.isNullOrBlank()
}
root.setOnClickListener { onItemClickListener(item) }
}

View File

@ -27,7 +27,7 @@ class TimetableAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (TimetableItemType.values()[viewType]) {
return when (TimetableItemType.entries[viewType]) {
TimetableItemType.SMALL -> SmallViewHolder(
ItemTimetableSmallBinding.inflate(inflater, parent, false)
)
@ -79,6 +79,7 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableSmallItemNumber.text = lesson.number.toString()
timetableSmallItemNumber.isVisible = item.isLessonNumberVisible
timetableSmallItemSubject.text = lesson.subject
timetableSmallItemTimeStart.text = lesson.start.toFormattedString("HH:mm")
timetableSmallItemRoom.text = lesson.room
@ -97,6 +98,7 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableItemNumber.text = lesson.number.toString()
timetableItemNumber.isVisible = item.isLessonNumberVisible
timetableItemSubject.text = lesson.subject
timetableItemGroup.text = lesson.group
timetableItemRoom.text = lesson.room

View File

@ -7,12 +7,14 @@ sealed class TimetableItem(val type: TimetableItemType) {
data class Small(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.SMALL)
data class Normal(
val lesson: Timetable,
val showGroupsInPlan: Boolean,
val isLessonNumberVisible: Boolean,
val timeLeft: TimeLeft?,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.NORMAL)

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.timetable
import android.os.Handler
import android.os.Looper
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
@ -58,6 +57,7 @@ class TimetablePresenter @Inject constructor(
private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
private var isEduOne: Boolean = false
var currentDate: LocalDate? = null
private set
@ -150,7 +150,8 @@ class TimetablePresenter @Inject constructor(
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester)
isEduOne = student.isEduOne == true
checkInitialAndCurrentDate(semester)
timetableRepository.getTimetable(
student = student,
semester = semester,
@ -194,9 +195,9 @@ class TimetablePresenter @Inject constructor(
.launch()
}
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
private suspend fun checkInitialAndCurrentDate(semester: Semester) {
if (initialDate == null) {
isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(student, semester)
isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester)
initialDate = getInitialDate(semester)
}
@ -235,9 +236,8 @@ class TimetablePresenter @Inject constructor(
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
it.isStudentPlan
} else true
}.sortedWith(
compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
)
}
.sortedWith(compareBy({ item -> item.start }, { item -> !item.isStudentPlan }))
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
@ -258,13 +258,15 @@ class TimetablePresenter @Inject constructor(
lesson = it,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
onClick = ::onTimetableItemSelected
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it,
onClick = ::onTimetableItemSelected
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(smallLesson)
}

View File

@ -11,7 +11,7 @@ import android.view.View.VISIBLE
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import io.github.wulkanowy.R
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.dataOrThrow
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
@ -27,6 +27,7 @@ import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Co
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.getErrorString
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.runBlocking
@ -45,11 +46,8 @@ class TimetableWidgetFactory(
) : RemoteViewsService.RemoteViewsFactory {
private var items = emptyList<TimetableWidgetItem>()
private var timetableCanceledColor: Int? = null
private var textColor: Int? = null
private var timetableChangeColor: Int? = null
override fun getLoadingView() = null
@ -67,25 +65,31 @@ class TimetableWidgetFactory(
override fun onDestroy() {}
override fun onDataSetChanged() {
intent?.extras?.getInt(EXTRA_APPWIDGET_ID)?.let { appWidgetId ->
val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0))
val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0)
val appWidgetId = intent?.extras?.getInt(EXTRA_APPWIDGET_ID) ?: return
val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0))
val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0)
items = emptyList()
runBlocking {
runCatching {
runBlocking {
val student = getStudent(studentId) ?: return@runBlocking
val semester = semesterRepository.getCurrentSemester(student)
items = createItems(
lessons = getLessons(student, semester, date),
lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date)
)
val student = getStudent(studentId) ?: return@runBlocking
val semester = semesterRepository.getCurrentSemester(student)
val lessons = getLessons(student, semester, date)
val lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date)
createItems(lessons, lastSync, !(student.isEduOne ?: false))
}
.onFailure {
items = listOf(TimetableWidgetItem.Error(it))
Timber.e(it, "An error has occurred in timetable widget factory")
}
.onSuccess {
items = it
if (date == LocalDate.now()) {
updateTodayLastLessonEnd(appWidgetId)
}
}
}.onFailure {
Timber.e(it, "An error has occurred in timetable widget factory")
}
}
}
@ -98,18 +102,20 @@ class TimetableWidgetFactory(
student: Student, semester: Semester, date: LocalDate
): List<Timetable> {
val timetable = timetableRepository.getTimetable(student, semester, date, date, false)
val lessons = timetable.toFirstResult().dataOrNull?.lessons.orEmpty()
return lessons.sortedBy { it.number }
val lessons = timetable.toFirstResult().dataOrThrow.lessons
return lessons.sortedBy { it.start }
}
private fun createItems(
lessons: List<Timetable>,
lastSync: Instant?,
isEduOne: Boolean
): List<TimetableWidgetItem> {
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
else -> null
}
return buildList {
lessons.forEach {
if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
@ -119,7 +125,7 @@ class TimetableWidgetFactory(
)
add(emptyItem)
}
add(TimetableWidgetItem.Normal(it))
add(TimetableWidgetItem.Normal(it, isEduOne))
prevNum = it.number
}
add(TimetableWidgetItem.Synchronized(lastSync ?: Instant.MIN))
@ -133,15 +139,12 @@ class TimetableWidgetFactory(
sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true)
}
companion object {
const val TIME_FORMAT_STYLE = "HH:mm"
}
override fun getViewAt(position: Int): RemoteViews? {
return when (val item = items.getOrNull(position) ?: return null) {
is TimetableWidgetItem.Normal -> getNormalItemRemoteView(item)
is TimetableWidgetItem.Empty -> getEmptyItemRemoteView(item)
is TimetableWidgetItem.Synchronized -> getSynchronizedItemRemoteView(item)
is TimetableWidgetItem.Error -> getErrorItemRemoteView(item)
}
}
@ -150,9 +153,11 @@ class TimetableWidgetFactory(
val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE)
val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE)
val lessonNumberVisibility = if (item.isLessonNumberVisible) VISIBLE else GONE
val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply {
setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString())
setViewVisibility(R.id.timetableWidgetItemNumber, lessonNumberVisibility)
setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime)
setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime)
setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject)
@ -213,6 +218,18 @@ class TimetableWidgetFactory(
}
}
private fun getErrorItemRemoteView(item: TimetableWidgetItem.Error): RemoteViews {
return RemoteViews(
context.packageName,
R.layout.item_widget_timetable_error
).apply {
setTextViewText(
R.id.timetable_widget_item_error_message,
context.resources.getErrorString(item.error)
)
}
}
private fun updateTheme() {
when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> {
@ -300,4 +317,8 @@ class TimetableWidgetFactory(
synchronizationTime,
)
}
private companion object {
private const val TIME_FORMAT_STYLE = "HH:mm"
}
}

View File

@ -7,6 +7,7 @@ sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) {
data class Normal(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
) : TimetableWidgetItem(TimetableWidgetItemType.NORMAL)
data class Empty(
@ -17,10 +18,15 @@ sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) {
data class Synchronized(
val timestamp: Instant,
) : TimetableWidgetItem(TimetableWidgetItemType.SYNCHRONIZED)
data class Error(
val error: Throwable
) : TimetableWidgetItem(TimetableWidgetItemType.ERROR)
}
enum class TimetableWidgetItemType {
NORMAL,
EMPTY,
SYNCHRONIZED,
ERROR,
}

View File

@ -4,30 +4,31 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.BundleCompat
import java.io.Serializable
// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios.
inline fun <reified T : Serializable> Bundle.serializable(key: String): T = when {
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!!
Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializable(key) as T
}
inline fun <reified T : Serializable> Bundle.nullableSerializable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)
Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as T?
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Parcelable> Bundle.parcelableArray(key: String): Array<T>? = when {
Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java)
else -> @Suppress("DEPRECATION") getParcelableArray(key) as Array<T>?
}
inline fun <reified T : Parcelable> Bundle.parcelableArray(key: String): Array<T>? =
BundleCompat.getParcelableArray(this, key, T::class.java) as Array<T>?
inline fun <reified T : Serializable> Intent.serializable(key: String): T = when {
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!!
Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T
}
inline fun <reified T : Serializable> Intent.nullableSerializable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)
Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T?
}

View File

@ -3,11 +3,13 @@ package io.github.wulkanowy.utils
import android.content.res.Resources
import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.AccountInactiveException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException
import io.github.wulkanowy.sdk.scrapper.exception.VulcanException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
import okhttp3.internal.http2.StreamResetException
@ -33,6 +35,8 @@ fun Resources.getErrorString(error: Throwable): String = when (error) {
is ServiceUnavailableException -> R.string.error_service_unavailable
is FeatureDisabledException -> R.string.error_feature_disabled
is FeatureNotAvailableException -> R.string.error_feature_not_available
is BadCredentialsException -> R.string.error_password_invalid
is AccountInactiveException -> R.string.error_account_inactive
is VulcanException -> R.string.error_unknown_uonet
is ScrapperException -> R.string.error_unknown_app
is CloudflareVerificationException -> R.string.error_cloudflare_captcha

View File

@ -1,42 +0,0 @@
package io.github.wulkanowy.utils
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
import timber.log.Timber
fun Sdk.init(student: Student): Sdk {
email = student.email
password = student.password
symbol = student.symbol
schoolSymbol = student.schoolSymbol
studentId = student.studentId
classId = student.classId
emptyCookieJarInterceptor = true
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
mobileBaseUrl = student.mobileBaseUrl
} else {
scrapperBaseUrl = student.scrapperBaseUrl
domainSuffix = student.scrapperDomainSuffix
loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
}
mode = Sdk.Mode.valueOf(student.loginMode)
mobileBaseUrl = student.mobileBaseUrl
keyId = student.certificateKey
privatePem = student.privateKey
Timber.d("Sdk in ${student.loginMode} mode reinitialized")
return this
}
fun Sdk.switchSemester(semester: Semester): Sdk {
return switchDiary(
diaryId = semester.diaryId,
kindergartenDiaryId = semester.kindergartenDiaryId,
schoolYear = semester.schoolYear,
unitId = semester.unitId,
)
}

View File

@ -18,7 +18,7 @@ fun Semester.isCurrent(now: LocalDate = now()): Boolean {
}
fun List<Semester>.getCurrentOrLast(): Semester {
if (isEmpty()) throw RuntimeException("Empty semester list")
if (isEmpty()) throw IllegalStateException("Empty semester list")
// when there is only one current semester
singleOrNull { it.isCurrent() }?.let { return it }

View File

@ -1,15 +1,31 @@
package io.github.wulkanowy.utils
import android.util.AndroidRuntimeException
import java.net.CookiePolicy
import java.net.CookieStore
import java.net.HttpCookie
import java.net.URI
import javax.inject.Inject
import javax.inject.Singleton
import android.webkit.CookieManager as WebkitCookieManager
import java.net.CookieManager as JavaCookieManager
class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
@Singleton
class WebkitCookieManagerProxy @Inject constructor() :
JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
private val webkitCookieManager: WebkitCookieManager = WebkitCookieManager.getInstance()
val webkitCookieManager: WebkitCookieManager? = getCookieManager()
/**
* @see [https://stackoverflow.com/a/70354583/6695449]
*/
private fun getCookieManager(): WebkitCookieManager? {
return try {
WebkitCookieManager.getInstance()
} catch (e: AndroidRuntimeException) {
null
}
}
override fun put(uri: URI?, responseHeaders: Map<String?, List<String?>>?) {
if (uri == null || responseHeaders == null) return
@ -23,7 +39,7 @@ class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL
// process each of the headers
for (headerValue in responseHeaders[headerKey].orEmpty()) {
webkitCookieManager.setCookie(url, headerValue)
webkitCookieManager?.setCookie(url, headerValue)
}
}
}
@ -34,7 +50,7 @@ class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL
): Map<String, List<String>> {
require(!(uri == null || requestHeaders == null)) { "Argument is null" }
val res = mutableMapOf<String, List<String>>()
val cookie = webkitCookieManager.getCookie(uri.toString())
val cookie = webkitCookieManager?.getCookie(uri.toString())
if (cookie != null) res["Cookie"] = listOf(cookie)
return res
}
@ -50,7 +66,7 @@ class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL
cookies.remove(uri, cookie)
override fun removeAll(): Boolean {
webkitCookieManager.removeAllCookies(null)
webkitCookieManager?.removeAllCookies(null) ?: return false
return true
}
}

View File

@ -1,8 +1,6 @@
Wersja 2.4.0
Wersja 2.5.5
— naprawiliśmy logowanie do aplikacji na odmianie standardowej
— naprawiliśmy wyświetlanie lekcji na kolejny dzień w kafelku na ekranie Start
— dodaliśmy oceny opisowe
— dodaliśmy kolorowe opisy we frekwencji we wpisach innych niż obecność
— naprawiliśmy migrację informacji o tym, czy szkoła ucznia używa eduOne
— naprawiliśmy w końcu (teraz naprawdę mamy taką nadzieję) ten komunikat o braku uprawnień
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorPrimary" />
<size
android:width="10dp"
android:height="10dp" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M14.12,10.47L12,12.59l-2.13,-2.12 -1.41,1.41L10.59,14l-2.12,2.12 1.41,1.41L12,15.41l2.12,2.12 1.41,-1.41L13.41,14l2.12,-2.12zM15.5,4l-1,-1h-5l-1,1H5v2h14V4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM8,9h8v10H8V9z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,14L8,9h8v10L8,19v-5zM10,18h4v-4h2l-4,-4 -4,4h2z" />
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="17dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="17dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,18.69L7.84,6.14 5.27,3.49 4,4.76l2.8,2.8v0.01c-0.52,0.99 -0.8,2.16 -0.8,3.42v5l-2,2v1h13.73l2,2L21,19.72l-1,-1.03zM12,22c1.11,0 2,-0.89 2,-2h-4c0,1.11 0.89,2 2,2zM18,14.68L18,11c0,-3.08 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68c-0.15,0.03 -0.29,0.08 -0.42,0.12 -0.1,0.03 -0.2,0.07 -0.3,0.11h-0.01c-0.01,0 -0.01,0 -0.02,0.01 -0.23,0.09 -0.46,0.2 -0.68,0.31 0,0 -0.01,0 -0.01,0.01L18,14.68z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More