1
0

Compare commits

...

116 Commits
2.3.0 ... 2.5.4

Author SHA1 Message Date
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
b99ba48d2c Merge branch 'release/2.4.0' 2024-02-09 19:45:01 +01:00
2d4a1bff83 Version 2.4.0 2024-02-09 19:44:55 +01:00
cd853e4d57 New Crowdin updates (#2417) 2024-02-09 18:34:04 +00:00
8183d7d5a0 Change default symbol for standard register variant (#2421) 2024-02-09 16:35:18 +01:00
3f199cb610 Replace fakelog.cf to wulkanowy.net.pl (#2419) 2024-02-09 12:40:01 +00:00
22f72981cb Add descriptive grades (#2411) 2024-02-08 09:16:09 +01:00
bce92b7347 Fix displaying lessons for tomorrow if there is no more lessons for today (#2416) 2024-02-08 07:49:17 +01:00
ed5166333a Add the ability to dynamically expand the application window (#2413) 2024-02-06 19:48:31 +01:00
a05f1f70f7 Add colors to attendance and ! to unexcused lateness (#2412) 2024-02-06 18:21:56 +01:00
e58a60410c Fix android tests (#2410) 2024-02-04 10:48:59 +01:00
fc91936884 Bump com.google.android.ump:user-messaging-platform from 2.1.0 to 2.2.0 (#2408) 2024-01-26 10:30:36 +00:00
88043569ac Bump com.huawei.hms:hianalytics from 6.12.0.300 to 6.12.0.301 (#2407) 2024-01-26 10:23:01 +00:00
10add8a70e Bump com.android.tools.build:gradle from 8.2.1 to 8.2.2 (#2406) 2024-01-26 10:21:30 +00:00
098af9884a Bump com.google.firebase:firebase-bom from 32.7.0 to 32.7.1 (#2404) 2024-01-23 19:17:29 +00:00
554c1b1261 Merge branch 'release/2.3.5' into develop 2024-01-21 21:04:35 +01:00
496695162d Merge branch 'release/2.3.5' 2024-01-21 21:04:29 +01:00
dc59f4ffa3 Version 2.3.5 2024-01-21 21:04:23 +01:00
e0f4cad7fb Add missing unitId to sdk switchSemester call (#2402) 2024-01-21 20:01:00 +01:00
a51a54dc7a Normalize synchronization date ranges to fix weird notification issues (#2403) 2024-01-21 18:59:54 +01:00
7cdac6ede1 Add admin message to error view in dashboard (#2400) 2024-01-21 12:39:23 +01:00
9dfb282e88 Add X to close admin message (#2401) 2024-01-21 11:39:55 +01:00
725668f855 Bump androidx.lifecycle:lifecycle-livedata-ktx from 2.6.2 to 2.7.0 (#2398) 2024-01-19 09:22:34 +00:00
e58c155961 New Crowdin updates (#2396) 2024-01-14 18:02:15 +01:00
05a5047a70 Merge branch 'release/2.3.4' into develop 2024-01-14 17:33:30 +01:00
1fe464a289 Merge branch 'release/2.3.4' 2024-01-14 17:33:14 +01:00
497acf9d68 Version 2.3.4 2024-01-14 17:32:41 +01:00
976eb5a772 Fix cancelling dashboard jobs (#2395) 2024-01-14 16:45:30 +01:00
9ececeb4e9 New Crowdin updates (#2394) 2024-01-14 16:41:57 +01:00
096fe359e7 Make some improvements in captcha dialog (#2393)
* Add improvements retrying after captcha solved

* Add showAuthDialog from BaseActivity instead of displaying this dialog manually

* Add getCookieStore() with removeAll impl in WebkitCookieManagerProxy

* Add debounce to captcha dialog showing logic

* Add refresh button to captcha dialog

* Destroy webview along with captcha dialog

* Add clear webkit cookies button to debug menu

* Add captcha error message

* Update captcha verified message
2024-01-14 13:09:04 +00:00
a98e8398fd Add webview to obtain cloudflare captcha cookies for okhttp (#2392) 2024-01-12 18:34:43 +01:00
d8c4926a97 Merge branch 'release/2.3.3' into develop 2024-01-09 21:46:10 +01:00
17c139b559 Merge branch 'release/2.3.3' 2024-01-09 21:46:04 +01:00
ddbcc7a04c Version 2.3.3 2024-01-09 21:45:59 +01:00
9f9eb60280 Merge branch 'release/2.3.2' into develop 2024-01-09 19:31:29 +01:00
f893170dec Merge branch 'release/2.3.2' 2024-01-09 19:31:22 +01:00
cff08d6322 Version 2.3.2 2024-01-09 19:27:03 +01:00
9dee7f01f6 Avoid deleting luckynumber when SDK returns null (#2391) 2024-01-09 19:07:46 +01:00
8324a9cac3 Use emptyCookieJarInterceptor in SDK configuration (#2390) 2024-01-09 19:00:37 +01:00
5316e3e1bf Bump mockk from 1.13.8 to 1.13.9 (#2389) 2024-01-08 15:32:30 +00:00
81e80181f2 New Crowdin updates (#2388) 2024-01-08 16:32:06 +01:00
6ee38e9259 Add clearing all data and key entry when decryption failed (#2386) 2024-01-06 00:01:33 +01:00
40df80371c Use forked slf4j-timber to fix logging problems with slf4j v2 (#2387) 2024-01-05 16:03:50 +01:00
a3596c35b8 Update AGP and Gradle (#2385) 2024-01-04 09:33:51 +01:00
66b7ea4cb4 Merge branch 'release/2.3.1' into develop 2024-01-03 16:02:39 +01:00
770749e158 Merge branch 'release/2.3.1' 2024-01-03 16:02:32 +01:00
0aa83b020e Bump sdk to 2.3.3 2024-01-03 16:01:30 +01:00
4d1218d1d3 Version 2.3.1 2024-01-03 14:53:16 +01:00
0ea6cbc8ed Merge branch 'release/2.3.0' into develop 2024-01-02 01:51:58 +01:00
224 changed files with 19752 additions and 3765 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

6
.gitignore vendored
View File

@ -65,6 +65,12 @@ captures/
.idea/uiDesigner.xml .idea/uiDesigner.xml
.idea/runConfigurations.xml .idea/runConfigurations.xml
.idea/discord.xml .idea/discord.xml
.idea/migrations.xml
.idea/androidTestResultsUserPreferences.xml
.idea/copilot
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.idea/kotlinc.xml
# Keystore files # Keystore files
*.jks *.jks

10
.idea/migrations.xml generated
View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode 140 versionCode 153
versionName "2.3.0" versionName "2.5.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
@ -142,7 +142,9 @@ android {
packagingOptions { packagingOptions {
resources { resources {
excludes += ['META-INF/library_release.kotlin_module', excludes += ['META-INF/library_release.kotlin_module',
'META-INF/library-core_release.kotlin_module'] 'META-INF/library-core_release.kotlin_module',
'META-INF/LICENSE.md',
'META-INF/LICENSE-notice.md']
} }
} }
@ -162,8 +164,8 @@ play {
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.15d userFraction = 0.99d
updatePriority = 3 updatePriority = 1
enabled.set(false) enabled.set(false)
} }
@ -185,19 +187,19 @@ huaweiPublish {
ext { ext {
work_manager = "2.9.0" work_manager = "2.9.0"
android_hilt = "1.1.0" android_hilt = "1.2.0"
room = "2.6.1" room = "2.6.1"
chucker = "4.0.0" chucker = "4.0.0"
mockk = "1.13.8" mockk = "1.13.10"
coroutines = "1.7.3" coroutines = "1.8.0"
} }
dependencies { dependencies {
implementation 'io.github.wulkanowy:sdk:2.3.1' implementation 'io.github.wulkanowy:sdk:2.5.4'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' 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 "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
@ -221,7 +223,7 @@ dependencies {
implementation "androidx.work:work-runtime:$work_manager" implementation "androidx.work:work-runtime:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room" implementation "androidx.room:room-ktx:$room"
@ -238,18 +240,19 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
implementation "com.jakewharton.timber:timber:5.0.1" implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.Faierbel:slf4j-timber:2.0'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2' implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" 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 "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.11.0' implementation 'org.apache.commons:commons-text:1.11.0'
playImplementation platform('com.google.firebase:firebase-bom:32.7.0') playImplementation platform('com.google.firebase:firebase-bom:32.7.3')
playImplementation 'com.google.firebase:firebase-analytics' playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
@ -261,7 +264,7 @@ dependencies {
playImplementation 'com.google.android.play:review-ktx:2.0.1' playImplementation 'com.google.android.play:review-ktx:2.0.1'
playImplementation "com.google.android.ump:user-messaging-platform:2.1.0" playImplementation "com.google.android.ump:user-messaging-platform:2.1.0"
hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300' hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303'
releaseImplementation "com.github.chuckerteam.chucker:library-no-op:$chucker" releaseImplementation "com.github.chuckerteam.chucker:library-no-op:$chucker"

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -14,34 +14,37 @@ import kotlin.test.assertFailsWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ScramblerTest { class ScramblerTest {
private val scrambler = Scrambler(ApplicationProvider.getApplicationContext())
@Test @Test
fun encryptDecryptTest() { fun encryptDecryptTest() {
assertEquals("TEST", decrypt(encrypt("TEST", assertEquals(
ApplicationProvider.getApplicationContext()))) "TEST", scrambler.decrypt(scrambler.encrypt("TEST"))
)
} }
@Test @Test
fun emptyTextEncryptTest() { fun emptyTextEncryptTest() {
assertFailsWith<ScramblerException> { assertFailsWith<ScramblerException> {
decrypt("") scrambler.decrypt("")
} }
assertFailsWith<ScramblerException> { assertFailsWith<ScramblerException> {
encrypt("", ApplicationProvider.getApplicationContext()) scrambler.encrypt("")
} }
} }
@Test @Test
@SdkSuppress(minSdkVersion = 18) @SdkSuppress(minSdkVersion = 18)
fun emptyKeyStoreTest() { fun emptyKeyStoreTest() {
val text = encrypt("test", ApplicationProvider.getApplicationContext()) val text = scrambler.encrypt("test")
val keyStore = KeyStore.getInstance("AndroidKeyStore") val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null) keyStore.load(null)
keyStore.deleteEntry("wulkanowy_password") keyStore.deleteEntry("wulkanowy_password")
assertFailsWith<ScramblerException> { assertFailsWith<ScramblerException> {
decrypt(text) scrambler.decrypt(text)
} }
} }
} }

View File

@ -44,6 +44,7 @@
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="false" android:supportsRtl="false"
android:theme="@style/WulkanowyTheme" android:theme="@style/WulkanowyTheme"
android:resizeableActivity="true"
tools:ignore="DataExtractionRules,UnusedAttribute"> tools:ignore="DataExtractionRules,UnusedAttribute">
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"

View File

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

View File

@ -18,16 +18,13 @@ import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.RemoteConfigHelper
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.create import retrofit2.create
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
@ -35,19 +32,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal class DataModule { 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) }
// for debug only
addInterceptor(chuckerInterceptor, network = true)
}
@Singleton @Singleton
@Provides @Provides
fun provideChuckerCollector( fun provideChuckerCollector(
@ -251,4 +235,12 @@ internal class DataModule {
@Singleton @Singleton
@Provides @Provides
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
@Singleton
@Provides
fun provideMutesDao(database: AppDatabase) = database.mutedMessageSendersDao
@Singleton
@Provides
fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao
} }

View File

@ -1,6 +1,16 @@
package io.github.wulkanowy.data package io.github.wulkanowy.data
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
@ -20,8 +30,15 @@ val <T> Resource<T>.dataOrNull: T?
get() = when (this) { get() = when (this) {
is Resource.Success -> this.data is Resource.Success -> this.data
is Resource.Intermediate -> this.data is Resource.Intermediate -> this.data
is Resource.Loading -> null else -> null
is Resource.Error -> 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? val <T> Resource<T>.errorOrNull: Throwable?
@ -131,7 +148,7 @@ inline fun <ResultType, RequestType> networkBoundResource(
query().map { Resource.Success(filterResult(it)) } query().map { Resource.Success(filterResult(it)) }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
onFetchFailed(throwable) onFetchFailed(throwable)
query().map { Resource.Error(throwable) } flowOf(Resource.Error(throwable))
} }
} else { } else {
query().map { Resource.Success(filterResult(it)) } query().map { Resource.Success(filterResult(it)) }
@ -165,7 +182,7 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
query().map { Resource.Success(mapResult(it)) } query().map { Resource.Success(mapResult(it)) }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
onFetchFailed(throwable) onFetchFailed(throwable)
query().map { Resource.Error(throwable) } flowOf(Resource.Error(throwable))
} }
} else { } else {
query().map { Resource.Success(mapResult(it)) } query().map { Resource.Success(mapResult(it)) }

View File

@ -0,0 +1,113 @@
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 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
eduOneMutex.withLock {
val studentFromDatabase = studentDb.loadById(student.id)
if (studentFromDatabase?.isEduOne != null) {
Timber.d("Migration eduOne: already done")
return studentFromDatabase.isEduOne
}
Timber.d("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.d("Migration eduOne: failed, so skipping")
return false
}
Timber.d("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

@ -1,11 +1,129 @@
package io.github.wulkanowy.data.db package io.github.wulkanowy.data.db
import android.content.Context import android.content.Context
import androidx.room.* import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import io.github.wulkanowy.data.db.dao.* import androidx.room.TypeConverters
import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.migrations.* import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao
import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
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
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.dao.StudentInfoDao
import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.dao.TeacherDao
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.AdminMessage
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeDescriptive
import io.github.wulkanowy.data.db.entities.GradePartialStatistics
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.LuckyNumber
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
import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Teacher
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.db.migrations.Migration10
import io.github.wulkanowy.data.db.migrations.Migration11
import io.github.wulkanowy.data.db.migrations.Migration12
import io.github.wulkanowy.data.db.migrations.Migration13
import io.github.wulkanowy.data.db.migrations.Migration14
import io.github.wulkanowy.data.db.migrations.Migration15
import io.github.wulkanowy.data.db.migrations.Migration16
import io.github.wulkanowy.data.db.migrations.Migration17
import io.github.wulkanowy.data.db.migrations.Migration18
import io.github.wulkanowy.data.db.migrations.Migration19
import io.github.wulkanowy.data.db.migrations.Migration2
import io.github.wulkanowy.data.db.migrations.Migration20
import io.github.wulkanowy.data.db.migrations.Migration21
import io.github.wulkanowy.data.db.migrations.Migration22
import io.github.wulkanowy.data.db.migrations.Migration23
import io.github.wulkanowy.data.db.migrations.Migration24
import io.github.wulkanowy.data.db.migrations.Migration25
import io.github.wulkanowy.data.db.migrations.Migration26
import io.github.wulkanowy.data.db.migrations.Migration27
import io.github.wulkanowy.data.db.migrations.Migration28
import io.github.wulkanowy.data.db.migrations.Migration29
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration30
import io.github.wulkanowy.data.db.migrations.Migration31
import io.github.wulkanowy.data.db.migrations.Migration32
import io.github.wulkanowy.data.db.migrations.Migration33
import io.github.wulkanowy.data.db.migrations.Migration34
import io.github.wulkanowy.data.db.migrations.Migration35
import io.github.wulkanowy.data.db.migrations.Migration36
import io.github.wulkanowy.data.db.migrations.Migration37
import io.github.wulkanowy.data.db.migrations.Migration38
import io.github.wulkanowy.data.db.migrations.Migration39
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration40
import io.github.wulkanowy.data.db.migrations.Migration41
import io.github.wulkanowy.data.db.migrations.Migration42
import io.github.wulkanowy.data.db.migrations.Migration43
import io.github.wulkanowy.data.db.migrations.Migration44
import io.github.wulkanowy.data.db.migrations.Migration46
import io.github.wulkanowy.data.db.migrations.Migration49
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration50
import io.github.wulkanowy.data.db.migrations.Migration51
import io.github.wulkanowy.data.db.migrations.Migration53
import io.github.wulkanowy.data.db.migrations.Migration54
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
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import javax.inject.Singleton import javax.inject.Singleton
@ -41,7 +159,9 @@ import javax.inject.Singleton
TimetableHeader::class, TimetableHeader::class,
SchoolAnnouncement::class, SchoolAnnouncement::class,
Notification::class, Notification::class,
AdminMessage::class AdminMessage::class,
MutedMessageSender::class,
GradeDescriptive::class,
], ],
autoMigrations = [ autoMigrations = [
AutoMigration(from = 44, to = 45), AutoMigration(from = 44, to = 45),
@ -51,6 +171,12 @@ import javax.inject.Singleton
AutoMigration(from = 54, to = 55, spec = Migration55::class), AutoMigration(from = 54, to = 55, spec = Migration55::class),
AutoMigration(from = 55, to = 56), AutoMigration(from = 55, to = 56),
AutoMigration(from = 56, to = 57, spec = Migration57::class), 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, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -59,7 +185,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 57 const val VERSION_SCHEMA = 63
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -184,4 +310,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract val notificationDao: NotificationDao abstract val notificationDao: NotificationDao
abstract val adminMessagesDao: AdminMessageDao 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.Dao
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.AdminMessage
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@Dao @Dao
abstract class AdminMessageDao : BaseDao<AdminMessage> { interface AdminMessageDao : BaseDao<AdminMessage> {
@Query("SELECT * FROM AdminMessages") @Query("SELECT * FROM AdminMessages")
abstract fun loadAll(): Flow<List<AdminMessage>> fun loadAll(): Flow<List<AdminMessage>>
@Transaction
open suspend fun removeOldAndSaveNew(
oldMessages: List<AdminMessage>,
newMessages: List<AdminMessage>
) {
deleteAll(oldMessages)
insertAll(newMessages)
}
} }

View File

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

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.GradeDescriptive
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface GradeDescriptiveDao : BaseDao<GradeDescriptive> {
@Query("SELECT * FROM GradesDescriptive WHERE semester_id = :semesterId AND student_id = :studentId")
fun loadAll(semesterId: Int, studentId: Int): Flow<List<GradeDescriptive>>
}

View File

@ -5,15 +5,23 @@ import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface MessagesDao : BaseDao<Message> { interface MessagesDao : BaseDao<Message> {
@Transaction @Transaction
@Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey") @Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?> 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") @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>> 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

@ -1,11 +1,18 @@
package io.github.wulkanowy.data.db.dao package io.github.wulkanowy.data.db.dao
import androidx.room.* import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@ -18,6 +25,12 @@ abstract class StudentDao {
@Delete @Delete
abstract suspend fun delete(student: Student) 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) @Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@ -47,6 +60,9 @@ abstract class StudentDao {
@Query("UPDATE Students SET is_current = 0") @Query("UPDATE Students SET is_current = 0")
abstract suspend fun resetCurrent() abstract suspend fun resetCurrent()
@Query("DELETE FROM Students WHERE email = :email AND user_name = :userName")
abstract suspend fun deleteByEmailAndUserName(email: String, userName: String)
@Transaction @Transaction
open suspend fun switchCurrent(id: Long) { open suspend fun switchCurrent(id: Long) {
resetCurrent() resetCurrent()

View File

@ -15,5 +15,5 @@ interface TimetableDao : BaseDao<Timetable> {
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<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") @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

@ -37,6 +37,9 @@ data class AdminMessage(
@ColumnInfo(name = "types", defaultValue = "[]") @ColumnInfo(name = "types", defaultValue = "[]")
val types: List<MessageType> = emptyList(), val types: List<MessageType> = emptyList(),
@ColumnInfo(name = "is_dismissible") @ColumnInfo(name = "is_ok_visible", defaultValue = "0")
val isDismissible: Boolean = false val isOkVisible: Boolean = false,
@ColumnInfo(name = "is_x_visible", defaultValue = "0")
val isXVisible: Boolean = false
) )

View File

@ -0,0 +1,27 @@
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 = "GradesDescriptive")
data class GradeDescriptive(
@ColumnInfo(name = "semester_id")
val semesterId: Int,
@ColumnInfo(name = "student_id")
val studentId: Int,
val subject: String,
val description: String,
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
}

View File

@ -2,11 +2,15 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Relation import androidx.room.Relation
import java.io.Serializable
data class MessageWithAttachment( data class MessageWithAttachment(
@Embedded @Embedded
val message: Message, val message: Message,
@Relation(parentColumn = "message_global_key", entityColumn = "message_global_key") @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 subject: String,
val content: String val content: String,
val author: String? = null,
) : Serializable { ) : Serializable {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)

View File

@ -78,6 +78,13 @@ data class Student(
@ColumnInfo(name = "registration_date") @ColumnInfo(name = "registration_date")
val registrationDate: Instant, val registrationDate: Instant,
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable { ) : Serializable {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ -88,3 +95,22 @@ data class Student(
@ColumnInfo(name = "avatar_color") @ColumnInfo(name = "avatar_color")
var avatarColor = 0L 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,10 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
@DeleteColumn(
tableName = "AdminMessages",
columnName = "is_dismissible",
)
class Migration58 : AutoMigrationSpec

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) { enum class MessageFolder(val id: Int = 1) {
RECEIVED(1), RECEIVED(1),
SENT(2), 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.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformation 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 { fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
SchoolAnnouncement( SchoolAnnouncement(
userLoginId = student.userLoginId, userLoginId = student.userLoginId,
date = it.date, date = it.date,
subject = it.subject, subject = it.subject,
content = it.content, 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

@ -1,10 +1,12 @@
package io.github.wulkanowy.data.mappers package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeDescriptive
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary
import io.github.wulkanowy.sdk.pojo.Grade as SdkGrade import io.github.wulkanowy.sdk.pojo.Grade as SdkGrade
import io.github.wulkanowy.sdk.pojo.GradeDescriptive as SdkGradeDescriptive
import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary
fun List<SdkGrade>.mapToEntities(semester: Semester) = map { fun List<SdkGrade>.mapToEntities(semester: Semester) = map {
Grade( Grade(
@ -40,3 +42,15 @@ fun List<SdkGradeSummary>.mapToEntities(semester: Semester) = map {
average = it.average average = it.average
) )
} }
@JvmName("mapGradeDescriptiveToEntities")
fun List<SdkGradeDescriptive>.mapToEntities(semester: Semester) = map {
GradeDescriptive(
semesterId = semester.semesterId,
studentId = semester.studentId,
subject = it.subject,
description = it.description
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,17 @@
package io.github.wulkanowy.data.repositories 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.dao.ExamDao
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.endExamsDay
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.startExamsDay
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
@ -17,7 +21,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class ExamRepository @Inject constructor( class ExamRepository @Inject constructor(
private val examDb: ExamDao, private val examDb: ExamDao,
private val sdk: Sdk, private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
@ -50,31 +54,32 @@ class ExamRepository @Inject constructor(
) )
}, },
fetch = { fetch = {
sdk.init(student) wulkanowySdkFactory.create(student, semester)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getExams(start.startExamsDay, start.endExamsDay) .getExams(start.startExamsDay, start.endExamsDay)
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
val examsToSave = (new uniqueSubtract old).onEach { examDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false if (notify) it.isNotified = false
} },
)
examDb.deleteAll(old uniqueSubtract new)
examDb.insertAll(examsToSave)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}, },
filterResult = { it.filter { item -> item.date in start..end } } filterResult = { it.filter { item -> item.date in start..end } }
) )
fun getExamsFromDatabase(semester: Semester, start: LocalDate): Flow<List<Exam>> { fun getExamsFromDatabase(
return examDb.loadAll( semester: Semester,
start: LocalDate,
end: LocalDate
): Flow<List<Exam>> = examDb.loadAll(
diaryId = semester.diaryId, diaryId = semester.diaryId,
studentId = semester.studentId, studentId = semester.studentId,
from = start.startExamsDay, from = start,
end = start.endExamsDay end = end,
) )
}
suspend fun updateExam(exam: List<Exam>) = examDb.updateAll(exam) suspend fun updateExam(exam: List<Exam>) = examDb.updateAll(exam)
} }

View File

@ -1,15 +1,20 @@
package io.github.wulkanowy.data.repositories 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.GradeDao
import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeDescriptive
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.* 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.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -22,14 +27,13 @@ import javax.inject.Singleton
class GradeRepository @Inject constructor( class GradeRepository @Inject constructor(
private val gradeDb: GradeDao, private val gradeDb: GradeDao,
private val gradeSummaryDb: GradeSummaryDao, private val gradeSummaryDb: GradeSummaryDao,
private val sdk: Sdk, private val gradeDescriptiveDb: GradeDescriptiveDao,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val cacheKey = "grade"
fun getGrades( fun getGrades(
student: Student, student: Student,
semester: Semester, semester: Semester,
@ -41,30 +45,53 @@ class GradeRepository @Inject constructor(
//When details is empty and summary is not, app will not use summary cache - edge case //When details is empty and summary is not, app will not use summary cache - edge case
it.first.isEmpty() it.first.isEmpty()
}, },
shouldFetch = { (details, summaries) -> shouldFetch = { (details, summaries, descriptive) ->
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) val isExpired =
details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired refreshHelper.shouldBeRefreshed(getRefreshKey(GRADE_CACHE_KEY, semester))
details.isEmpty() || (summaries.isEmpty() && descriptive.isEmpty()) || forceRefresh || isExpired
}, },
query = { query = {
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries } val descriptiveFlow =
gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId)
combine(detailsFlow, summaryFlow, descriptiveFlow) { details, summaries, descriptive ->
Triple(details, summaries, descriptive)
}
}, },
fetch = { fetch = {
val (details, summary) = sdk.init(student) val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getGrades(semester.semesterId) .getGrades(semester.semesterId)
details.mapToEntities(semester) to summary.mapToEntities(semester) Triple(
details.mapToEntities(semester),
summary.mapToEntities(semester),
descriptive.mapToEntities(semester)
)
}, },
saveFetchResult = { (oldDetails, oldSummary), (newDetails, newSummary) -> saveFetchResult = { (oldDetails, oldSummary, oldDescriptive), (newDetails, newSummary, newDescriptive) ->
refreshGradeDetails(student, oldDetails, newDetails, notify) refreshGradeDetails(student, oldDetails, newDetails, notify)
refreshGradeSummaries(oldSummary, newSummary, notify) refreshGradeSummaries(oldSummary, newSummary, notify)
refreshGradeDescriptions(oldDescriptive, newDescriptive, notify)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(GRADE_CACHE_KEY, semester))
} }
) )
private suspend fun refreshGradeDescriptions(
old: List<GradeDescriptive>,
new: List<GradeDescriptive>,
notify: Boolean
) {
gradeDescriptiveDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
},
)
}
private suspend fun refreshGradeDetails( private suspend fun refreshGradeDetails(
student: Student, student: Student,
oldGrades: List<Grade>, oldGrades: List<Grade>,
@ -73,13 +100,16 @@ class GradeRepository @Inject constructor(
) { ) {
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date
?: student.registrationDate.toLocalDate() ?: student.registrationDate.toLocalDate()
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { gradeDb.removeOldAndSaveNew(
oldItems = oldGrades uniqueSubtract newDetails,
newItems = (newDetails uniqueSubtract oldGrades).onEach {
if (it.date >= notifyBreakDate) it.apply { if (it.date >= notifyBreakDate) it.apply {
isRead = false isRead = false
if (notify) isNotified = false if (notify) isNotified = false
} }
}) },
)
} }
private suspend fun refreshGradeSummaries( private suspend fun refreshGradeSummaries(
@ -87,9 +117,23 @@ class GradeRepository @Inject constructor(
newSummary: List<GradeSummary>, newSummary: List<GradeSummary>,
notify: Boolean notify: Boolean
) { ) {
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) gradeSummaryDb.removeOldAndSaveNew(
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> oldItems = oldSummaries uniqueSubtract newSummary,
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject } newItems = (newSummary uniqueSubtract oldSummaries).onEach { summary ->
getGradeSummaryWithUpdatedNotificationState(
summary = summary,
oldSummary = oldSummaries.find { it.subject == summary.subject },
notify = notify,
)
},
)
}
private fun getGradeSummaryWithUpdatedNotificationState(
summary: GradeSummary,
oldSummary: GradeSummary?,
notify: Boolean,
) {
summary.isPredictedGradeNotified = when { summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
@ -100,7 +144,6 @@ class GradeRepository @Inject constructor(
notify && oldSummary?.finalGrade != summary.finalGrade -> false notify && oldSummary?.finalGrade != summary.finalGrade -> false
else -> true else -> true
} }
summary.predictedGradeLastChange = when { summary.predictedGradeLastChange = when {
oldSummary == null -> Instant.now() oldSummary == null -> Instant.now()
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now() summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
@ -111,7 +154,6 @@ class GradeRepository @Inject constructor(
summary.finalGrade != oldSummary.finalGrade -> Instant.now() summary.finalGrade != oldSummary.finalGrade -> Instant.now()
else -> oldSummary.finalGradeLastChange else -> oldSummary.finalGradeLastChange
} }
})
} }
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> { fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
@ -132,6 +174,10 @@ class GradeRepository @Inject constructor(
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
} }
fun getGradesDescriptiveFromDatabase(semester: Semester): Flow<List<GradeDescriptive>> {
return gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId)
}
suspend fun updateGrade(grade: Grade) { suspend fun updateGrade(grade: Grade) {
return gradeDb.updateAll(listOf(grade)) return gradeDb.updateAll(listOf(grade))
} }
@ -143,4 +189,13 @@ class GradeRepository @Inject constructor(
suspend fun updateGradesSummary(gradesSummary: List<GradeSummary>) { suspend fun updateGradesSummary(gradesSummary: List<GradeSummary>) {
return gradeSummaryDb.updateAll(gradesSummary) return gradeSummaryDb.updateAll(gradesSummary)
} }
suspend fun updateGradesDescriptive(gradesDescriptive: List<GradeDescriptive>) {
return gradeDescriptiveDb.updateAll(gradesDescriptive)
}
private companion object {
private const val GRADE_CACHE_KEY = "grade"
}
} }

View File

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

View File

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

View File

@ -1,12 +1,11 @@
package io.github.wulkanowy.data.repositories 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.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource 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.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -18,7 +17,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class LuckyNumberRepository @Inject constructor( class LuckyNumberRepository @Inject constructor(
private val luckyNumberDb: LuckyNumberDao, private val luckyNumberDb: LuckyNumberDao,
private val sdk: Sdk private val wulkanowySdkFactory: WulkanowySdkFactory,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
@ -33,14 +32,18 @@ class LuckyNumberRepository @Inject constructor(
shouldFetch = { it == null || forceRefresh }, shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) }, query = { luckyNumberDb.load(student.studentId, now()) },
fetch = { fetch = {
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) wulkanowySdkFactory.create(student)
.getLuckyNumber(student.schoolShortName)
?.mapToEntity(student)
}, },
saveFetchResult = { old, new -> saveFetchResult = { oldLuckyNumber, newLuckyNumber ->
if (new != old) { newLuckyNumber ?: return@networkBoundResource
old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
luckyNumberDb.insertAll(listOfNotNull((new?.apply { if (newLuckyNumber != oldLuckyNumber) {
if (notify) isNotified = false 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 dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource 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.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao 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.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment 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.Recipient
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED 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.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities 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.onResourceError
import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.MessageDraft import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -42,8 +45,9 @@ import javax.inject.Singleton
@Singleton @Singleton
class MessageRepository @Inject constructor( class MessageRepository @Inject constructor(
private val messagesDb: MessagesDao, private val messagesDb: MessagesDao,
private val mutedMessageSendersDao: MutedMessageSendersDao,
private val messageAttachmentDao: MessageAttachmentDao, private val messageAttachmentDao: MessageAttachmentDao,
private val sdk: Sdk, private val wulkanowySdkFactory: WulkanowySdkFactory,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider, private val sharedPrefProvider: SharedPrefProvider,
@ -51,7 +55,6 @@ class MessageRepository @Inject constructor(
private val mailboxDao: MailboxDao, private val mailboxDao: MailboxDao,
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase, private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val messagesCacheKey = "message" private val messagesCacheKey = "message"
@ -63,7 +66,7 @@ class MessageRepository @Inject constructor(
folder: MessageFolder, folder: MessageFolder,
forceRefresh: Boolean, forceRefresh: Boolean,
notify: Boolean = false, notify: Boolean = false,
): Flow<Resource<List<Message>>> = networkBoundResource( ): Flow<Resource<List<MessageWithMutedAuthor>>> = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() }, isResultEmpty = { it.isEmpty() },
shouldFetch = { shouldFetch = {
@ -74,21 +77,30 @@ class MessageRepository @Inject constructor(
}, },
query = { query = {
if (mailbox == null) { if (mailbox == null) {
messagesDb.loadAll(folder.id, student.email) messagesDb.loadMessagesWithMutedAuthor(folder.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, folder.id) } else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id)
}, },
fetch = { fetch = {
sdk.init(student).getMessages( wulkanowySdkFactory.create(student)
.getMessages(
folder = Folder.valueOf(folder.name), folder = Folder.valueOf(folder.name),
mailboxKey = mailbox?.globalKey, mailboxKey = mailbox?.globalKey,
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email)) )
.mapToEntities(
student = student,
mailbox = mailbox,
allMailboxes = mailboxDao.loadAll(student.email)
)
}, },
saveFetchResult = { old, new -> saveFetchResult = { oldWithAuthors, new ->
messagesDb.deleteAll(old uniqueSubtract new) val old = oldWithAuthors.map { it.message }
messagesDb.insertAll((new uniqueSubtract old).onEach { messagesDb.removeOldAndSaveNew(
it.isNotified = !notify oldItems = old uniqueSubtract new,
}) newItems = (new uniqueSubtract old).onEach {
val muted = isMuted(it.correspondents)
it.isNotified = !notify || muted
},
)
refreshHelper.updateLastRefreshTimestamp( refreshHelper.updateLastRefreshTimestamp(
getRefreshKey(messagesCacheKey, mailbox, folder) getRefreshKey(messagesCacheKey, mailbox, folder)
) )
@ -106,11 +118,10 @@ class MessageRepository @Inject constructor(
Timber.d("Message content in db empty: ${it.message.content.isBlank()}") Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
(it.message.unread && markAsRead) || it.message.content.isBlank() (it.message.unread && markAsRead) || it.message.content.isBlank()
}, },
query = { query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
},
fetch = { fetch = {
sdk.init(student).getMessageDetails( wulkanowySdkFactory.create(student)
.getMessageDetails(
messageKey = it!!.message.messageGlobalKey, messageKey = it!!.message.messageGlobalKey,
markAsRead = message.unread && markAsRead, markAsRead = message.unread && markAsRead,
) )
@ -152,19 +163,33 @@ class MessageRepository @Inject constructor(
subject: String, subject: String,
content: String, content: String,
recipients: List<Recipient>, recipients: List<Recipient>,
mailboxId: String, mailbox: Mailbox,
) { ) {
sdk.init(student).sendMessage( wulkanowySdkFactory.create(student)
.sendMessage(
subject = subject, subject = subject,
content = content, content = content,
recipients = recipients.mapFromEntities(), recipients = recipients.mapFromEntities(),
mailboxId = mailboxId, 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() val firstMessage = messages.first()
sdk.init(student).deleteMessages( wulkanowySdkFactory.create(student)
.deleteMessages(
messages = messages.map { it.messageGlobalKey }, messages = messages.map { it.messageGlobalKey },
removeForever = firstMessage.folderId == TRASHED.id, removeForever = firstMessage.folderId == TRASHED.id,
) )
@ -181,18 +206,24 @@ class MessageRepository @Inject constructor(
} }
messagesDb.updateAll(deletedMessages) messagesDb.updateAll(deletedMessages)
} else messagesDb.deleteAll(messages) } else {
messagesDb.deleteAll(messages)
}
}
private suspend fun refreshFolders(
student: Student,
mailbox: Mailbox?,
folders: List<MessageFolder> = MessageFolder.entries
) {
folders.forEach {
getMessages( getMessages(
student = student, student = student,
mailbox = mailbox, mailbox = mailbox,
folder = TRASHED, folder = it,
forceRefresh = true, forceRefresh = true,
).first() ).toFirstResult()
} }
suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
deleteMessages(student, mailbox, listOf(message))
} }
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource( 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) }, query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) },
fetch = { fetch = {
sdk.init(student).getMailboxes().mapToEntities(student) wulkanowySdkFactory.create(student)
.getMailboxes()
.mapToEntities(student)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
mailboxDao.deleteAll(old uniqueSubtract new) mailboxDao.deleteAll(old uniqueSubtract new)
@ -236,4 +269,18 @@ class MessageRepository @Inject constructor(
context.getString(R.string.pref_key_message_draft), context.getString(R.string.pref_key_message_draft),
value?.let { json.encodeToString(it) } 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 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.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
@ -8,10 +9,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToMobileDeviceToken import io.github.wulkanowy.data.mappers.mapToMobileDeviceToken
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.MobileDeviceToken import io.github.wulkanowy.data.pojos.MobileDeviceToken
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
@ -20,7 +19,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class MobileDeviceRepository @Inject constructor( class MobileDeviceRepository @Inject constructor(
private val mobileDb: MobileDeviceDao, private val mobileDb: MobileDeviceDao,
private val sdk: Sdk, private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
@ -41,30 +40,28 @@ class MobileDeviceRepository @Inject constructor(
}, },
query = { mobileDb.loadAll(student.userLoginId) }, query = { mobileDb.loadAll(student.userLoginId) },
fetch = { fetch = {
sdk.init(student) wulkanowySdkFactory.create(student, semester)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getRegisteredDevices() .getRegisteredDevices()
.mapToEntities(student) .mapToEntities(student)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
mobileDb.deleteAll(old uniqueSubtract new) mobileDb.removeOldAndSaveNew(
mobileDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
) )
suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) { suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) {
sdk.init(student) wulkanowySdkFactory.create(student, semester)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.unregisterDevice(device.deviceId) .unregisterDevice(device.deviceId)
mobileDb.deleteAll(listOf(device)) mobileDb.deleteAll(listOf(device))
} }
suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken { suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken {
return sdk.init(student) return wulkanowySdkFactory.create(student, semester)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getToken() .getToken()
.mapToMobileDeviceToken() .mapToMobileDeviceToken()
} }

View File

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

View File

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

View File

@ -1,17 +1,23 @@
package io.github.wulkanowy.data.repositories 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@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> { suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair<String, String> =
return sdk.getPasswordResetCaptchaCode(host, symbol) wulkanowySdkFactory.create()
} .getPasswordResetCaptchaCode(host, symbol)
suspend fun sendRecoverRequest( suspend fun sendRecoverRequest(
url: String, symbol: String, email: String, reCaptchaResponse: String url: String,
): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) 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 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.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -18,7 +17,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class SchoolAnnouncementRepository @Inject constructor( class SchoolAnnouncementRepository @Inject constructor(
private val schoolAnnouncementDb: SchoolAnnouncementDao, private val schoolAnnouncementDb: SchoolAnnouncementDao,
private val sdk: Sdk, private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
@ -41,17 +40,18 @@ class SchoolAnnouncementRepository @Inject constructor(
schoolAnnouncementDb.loadAll(student.userLoginId) schoolAnnouncementDb.loadAll(student.userLoginId)
}, },
fetch = { fetch = {
sdk.init(student) val sdk = wulkanowySdkFactory.create(student)
.getDirectorInformation() val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student)
.mapToEntities(student) val directorInformation = sdk.getDirectorInformation().mapToEntities(student)
lastAnnouncements + directorInformation
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach { schoolAnnouncementDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false if (notify) it.isNotified = false
} },
)
schoolAnnouncementDb.deleteAll(old uniqueSubtract new)
schoolAnnouncementDb.insertAll(schoolAnnouncementsToSave)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +1,36 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import android.content.Context
import androidx.room.withTransaction import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException 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.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.security.Scrambler
import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class StudentRepository @Inject constructor( class StudentRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val dispatchers: DispatchersProvider, private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val semesterDb: SemesterDao, private val semesterDb: SemesterDao,
private val sdk: Sdk, private val wulkanowySdkFactory: WulkanowySdkFactory,
private val appDatabase: AppDatabase private val appDatabase: AppDatabase,
private val scrambler: Scrambler,
) { ) {
suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false
@ -39,9 +39,10 @@ class StudentRepository @Inject constructor(
pin: String, pin: String,
symbol: String, symbol: String,
token: String token: String
): RegisterUser = sdk ): RegisterUser = wulkanowySdkFactory.create()
.getStudentsFromHebe(token, pin, symbol, "") .getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null) .mapToPojo(null)
.also { it.logErrors() }
suspend fun getUserSubjectsFromScrapper( suspend fun getUserSubjectsFromScrapper(
email: String, email: String,
@ -49,18 +50,20 @@ class StudentRepository @Inject constructor(
scrapperBaseUrl: String, scrapperBaseUrl: String,
domainSuffix: String, domainSuffix: String,
symbol: String symbol: String
): RegisterUser = sdk ): RegisterUser = wulkanowySdkFactory.create()
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol) .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password) .mapToPojo(password)
.also { it.logErrors() }
suspend fun getStudentsHybrid( suspend fun getStudentsHybrid(
email: String, email: String,
password: String, password: String,
scrapperBaseUrl: String, scrapperBaseUrl: String,
symbol: String symbol: String
): RegisterUser = sdk ): RegisterUser = wulkanowySdkFactory.create()
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) .getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password) .mapToPojo(password)
.also { it.logErrors() }
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> { suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) -> return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
@ -68,7 +71,7 @@ class StudentRepository @Inject constructor(
student = student.apply { student = student.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
}, },
@ -86,7 +89,7 @@ class StudentRepository @Inject constructor(
}.apply { }.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
} }
@ -96,18 +99,58 @@ class StudentRepository @Inject constructor(
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
return student 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 { suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
return student return student
@ -120,7 +163,7 @@ class StudentRepository @Inject constructor(
it.apply { it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) {
password = withContext(dispatchers.io) { password = withContext(dispatchers.io) {
encrypt(password, context) scrambler.encrypt(password)
} }
} }
} }
@ -151,19 +194,48 @@ class StudentRepository @Inject constructor(
.distinctBy { it.student.studentName }.size == 1 .distinctBy { it.student.studentName }.size == 1
suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) = suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) =
sdk.init(student) wulkanowySdkFactory.create(student, semester)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.authorizePermission(pesel) .authorizePermission(pesel)
suspend fun refreshStudentName(student: Student, semester: Semester) { suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val newCurrentApiStudent = sdk.init(student) val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
.getCurrentStudent() ?: return .onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
.getOrNull() ?: return
val studentName = StudentName( val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}" studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id } ).apply { id = student.id }
studentDb.update(studentName) studentDb.update(studentName)
semesterDb.removeOldAndSaveNew(
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
)
}
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
studentDb.deleteByEmailAndUserName(student.email, student.userName)
}
suspend fun clearAll() {
withContext(dispatchers.io) {
scrambler.clearKeyPair()
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,14 +1,13 @@
package io.github.wulkanowy.data.repositories 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.dao.SubjectDao
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
@ -17,7 +16,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class SubjectRepository @Inject constructor( class SubjectRepository @Inject constructor(
private val subjectDao: SubjectDao, private val subjectDao: SubjectDao,
private val sdk: Sdk, private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
) { ) {
@ -38,14 +37,15 @@ class SubjectRepository @Inject constructor(
}, },
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = { fetch = {
sdk.init(student) wulkanowySdkFactory.create(student, semester)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .getSubjects()
.getSubjects().mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
subjectDao.deleteAll(old uniqueSubtract new) subjectDao.removeOldAndSaveNew(
subjectDao.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )

View File

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

View File

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

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.domain.timetable
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import java.time.LocalDate
import javax.inject.Inject
class IsStudentHasLessonsOnWeekendUseCase @Inject constructor(
private val timetableRepository: TimetableRepository,
private val isWeekendHasLessonsUseCase: IsWeekendHasLessonsUseCase,
) {
suspend operator fun invoke(
semester: Semester,
currentDate: LocalDate = LocalDate.now(),
): Boolean {
val lessons = timetableRepository.getTimetableFromDatabase(
semester = semester,
start = currentDate.monday,
end = currentDate.sunday,
)
return isWeekendHasLessonsUseCase(lessons)
}
}

View File

@ -0,0 +1,17 @@
package io.github.wulkanowy.domain.timetable
import io.github.wulkanowy.data.db.entities.Timetable
import java.time.DayOfWeek
import javax.inject.Inject
class IsWeekendHasLessonsUseCase @Inject constructor() {
operator fun invoke(
lessons: List<Timetable>,
): Boolean = lessons.any {
it.date.dayOfWeek in listOf(
DayOfWeek.SATURDAY,
DayOfWeek.SUNDAY,
)
}
}

View File

@ -65,8 +65,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
range = lesson.start..lesson.end, range = lesson.start..lesson.end,
requestCode = getRequestCode(lesson.start, studentId) requestCode = getRequestCode(lesson.start, studentId)
) )
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
} }
} }
} }

View File

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

View File

@ -4,12 +4,12 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeDescriptive
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject import javax.inject.Inject
@ -88,4 +88,28 @@ class NewGradeNotification @Inject constructor(
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
} }
suspend fun notifyDescriptive(items: List<GradeDescriptive>, student: Student) {
val notificationDataList = items.map {
NotificationData(
title = context.getPlural(R.plurals.grade_new_items_descriptive, 1),
content = "${it.subject}: ${it.description}",
destination = Destination.Grade,
)
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(R.plurals.grade_new_items_descriptive, items.size),
content = context.getPlural(
R.plurals.grade_notify_new_items_descriptive,
items.size,
items.size
),
destination = Destination.Grade,
type = NotificationType.NEW_GRADE_DESCRIPTIVE
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
}
} }

View File

@ -37,6 +37,10 @@ enum class NotificationType(
channel = NewGradesChannel.CHANNEL_ID, channel = NewGradesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_grade, icon = R.drawable.ic_stat_grade,
), ),
NEW_GRADE_DESCRIPTIVE(
channel = NewGradesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_grade,
),
NEW_HOMEWORK( NEW_HOMEWORK(
channel = NewHomeworkChannel.CHANNEL_ID, channel = NewHomeworkChannel.CHANNEL_ID,
icon = R.drawable.ic_more_homework, icon = R.drawable.ic_more_homework,

View File

@ -16,17 +16,24 @@ class AttendanceWork @Inject constructor(
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val startDate = now().previousOrSameSchoolDay
val endDate = startDate.plusDays(7)
attendanceRepository.getAttendance( attendanceRepository.getAttendance(
student = student, student = student,
semester = semester, semester = semester,
start = now().previousOrSameSchoolDay, start = startDate,
end = now().previousOrSameSchoolDay, end = endDate,
forceRefresh = true, forceRefresh = true,
notify = notify, notify = notify,
) )
.waitForResult() .waitForResult()
attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now()) attendanceRepository.getAttendanceFromDatabase(
semester = semester,
start = startDate,
end = endDate,
)
.first() .first()
.filterNot { it.isNotified } .filterNot { it.isNotified }
.let { .let {

View File

@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.NewExamNotification import io.github.wulkanowy.services.sync.notifications.NewExamNotification
import io.github.wulkanowy.utils.endExamsDay
import io.github.wulkanowy.utils.startExamsDay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
@ -15,16 +17,24 @@ class ExamWork @Inject constructor(
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val startDate = now().startExamsDay
val endDate = startDate.endExamsDay
examRepository.getExams( examRepository.getExams(
student = student, student = student,
semester = semester, semester = semester,
start = now(), start = startDate,
end = now(), end = endDate,
forceRefresh = true, forceRefresh = true,
notify = notify, notify = notify,
).waitForResult() ).waitForResult()
examRepository.getExamsFromDatabase(semester, now()).first() examRepository.getExamsFromDatabase(
semester = semester,
start = startDate,
end = endDate,
)
.first()
.filter { !it.isNotified }.let { .filter { !it.isNotified }.let {
if (it.isNotEmpty()) newExamNotification.notify(it, student) if (it.isNotEmpty()) newExamNotification.notify(it, student)

View File

@ -45,5 +45,15 @@ class GradeWork @Inject constructor(
grade.isFinalGradeNotified = true grade.isFinalGradeNotified = true
}) })
} }
gradeRepository.getGradesDescriptiveFromDatabase(semester).first()
.filter { !it.isNotified }
.let {
if (it.isNotEmpty()) newGradeNotification.notifyDescriptive(it, student)
gradeRepository.updateGradesDescriptive(it.onEach { grade ->
grade.isNotified = true
})
}
} }
} }

View File

@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.sunday
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
@ -16,16 +18,24 @@ class HomeworkWork @Inject constructor(
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val startDate = now().nextOrSameSchoolDay.monday
val endDate = startDate.sunday
homeworkRepository.getHomework( homeworkRepository.getHomework(
student = student, student = student,
semester = semester, semester = semester,
start = now().nextOrSameSchoolDay, start = startDate,
end = now().nextOrSameSchoolDay, end = endDate,
forceRefresh = true, forceRefresh = true,
notify = notify, notify = notify,
).waitForResult() ).waitForResult()
homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).first() homeworkRepository.getHomeworkFromDatabase(
semester = semester,
start = startDate,
end = endDate
)
.first()
.filter { !it.isNotified }.let { .filter { !it.isNotified }.let {
if (it.isNotEmpty()) newHomeworkNotification.notify(it, student) if (it.isNotEmpty()) newHomeworkNotification.notify(it, student)

View File

@ -6,7 +6,6 @@ import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
@ -16,18 +15,24 @@ class TimetableWork @Inject constructor(
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val startDate = now().nextOrSameSchoolDay
val endDate = startDate.plusDays(7)
timetableRepository.getTimetable( timetableRepository.getTimetable(
student = student, student = student,
semester = semester, semester = semester,
start = now().nextOrSameSchoolDay, start = startDate,
end = now().nextOrSameSchoolDay, end = endDate,
forceRefresh = true, forceRefresh = true,
notify = notify, notify = notify,
) )
.waitForResult() .waitForResult()
timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7)) timetableRepository.getTimetableFromDatabase(
.first() semester = semester,
start = startDate,
end = endDate,
)
.filterNot { it.isNotified } .filterNot { it.isNotified }
.let { .let {
if (it.isNotEmpty()) changeTimetableNotification.notify(it, student) if (it.isNotEmpty()) changeTimetableNotification.notify(it, student)

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import android.app.ActivityManager import android.app.ActivityManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
@ -11,11 +12,14 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.FragmentLifecycleLogger import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> : abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
@ -35,16 +39,26 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
abstract var presenter: T abstract var presenter: T
private var lastDialogOpenTime = mutableMapOf<String, Instant>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
inject() inject()
themeManager.applyActivityTheme(this) themeManager.applyActivityTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
applyCustomTaskDescription()
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
setTaskDescription( private fun applyCustomTaskDescription() {
ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface)) 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) { override fun showError(text: String, error: Throwable) {
@ -68,11 +82,28 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
} else Toast.makeText(this, text, Toast.LENGTH_LONG).show() } else Toast.makeText(this, text, Toast.LENGTH_LONG).show()
} }
override fun showExpiredDialog() { 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)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmExpiredCredentialsSelected() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
override fun onCaptchaVerificationRequired(url: String?) {
CaptchaDialog.newInstance(url).show(supportFragmentManager, "captcha_dialog")
}
override fun showDecryptionFailedDialog() {
if (!shouldShowDialog(DIALOG_ERROR_DECRYPTION_FAILED)) return
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired) .setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin) .setMessage(R.string.main_session_relogin)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() } .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmDecryptionFailedSelected() }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
.show() .show()
} }
@ -105,4 +136,21 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
protected open fun inject() { protected open fun inject() {
throw UnsupportedOperationException() 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

@ -8,7 +8,6 @@ import android.widget.Toast
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import javax.inject.Inject import javax.inject.Inject
@ -28,8 +27,16 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
Toast.makeText(context, text, Toast.LENGTH_LONG).show() Toast.makeText(context, text, Toast.LENGTH_LONG).show()
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun openClearLoginView() { override fun openClearLoginView() {
@ -41,7 +48,7 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
} }
override fun showAuthDialog() { override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") (activity as? BaseActivity<*, *>)?.showAuthDialog()
} }
override fun showErrorDetailsDialog(error: Throwable) { override fun showErrorDetailsDialog(error: Throwable) {

View File

@ -7,7 +7,6 @@ import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragment(layoutId), abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragment(layoutId),
@ -39,12 +38,20 @@ abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragme
} }
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun showAuthDialog() { override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") (activity as? BaseActivity<*, *>)?.showAuthDialog()
} }
override fun openClearLoginView() { override fun openClearLoginView() {

View File

@ -28,20 +28,38 @@ open class BasePresenter<T : BaseView>(
this.view = view this.view = view
errorHandler.apply { errorHandler.apply {
showErrorMessage = view::showError showErrorMessage = view::showError
onSessionExpired = view::showExpiredDialog onExpiredCredentials = view::showExpiredCredentialsDialog
onCaptchaVerificationRequired = view::onCaptchaVerificationRequired
onDecryptionFailed = view::showDecryptionFailedDialog
onNoCurrentStudent = view::openClearLoginView onNoCurrentStudent = view::openClearLoginView
onPasswordChangeRequired = view::showChangePasswordSnackbar onPasswordChangeRequired = view::showChangePasswordSnackbar
onAuthorizationRequired = view::showAuthDialog onAuthorizationRequired = view::showAuthDialog
} }
} }
fun onExpiredLoginSelected() { fun onConfirmDecryptionFailedSelected() {
Timber.i("Attempt to switch the student after the session expires") Timber.i("Attempt to clear all data")
presenterScope.launch {
runCatching { studentRepository.clearAll() }
.onFailure {
Timber.i("Clear data result: An exception occurred")
errorHandler.dispatch(it)
}
.onSuccess {
Timber.i("Clear data result: Open login view")
view?.openClearLoginView()
}
}
}
fun onConfirmExpiredCredentialsSelected() {
Timber.i("Attempt to delete students associated with the account and switch to new student")
presenterScope.launch { presenterScope.launch {
runCatching { runCatching {
val student = studentRepository.getCurrentStudent(false) val student = studentRepository.getCurrentStudent(false)
studentRepository.logoutStudent(student) studentRepository.deleteStudentsAssociatedWithAccount(student)
val students = studentRepository.getSavedStudents(false) val students = studentRepository.getSavedStudents(false)
if (students.isNotEmpty()) { if (students.isNotEmpty()) {
@ -50,11 +68,11 @@ open class BasePresenter<T : BaseView>(
} }
} }
.onFailure { .onFailure {
Timber.i("Switch student result: An exception occurred") Timber.i("Delete students result: An exception occurred")
errorHandler.dispatch(it) errorHandler.dispatch(it)
} }
.onSuccess { .onSuccess {
Timber.i("Switch student result: Open login view") Timber.i("Delete students result: Open login view")
view?.openClearLoginView() view?.openClearLoginView()
} }
} }

View File

@ -6,7 +6,11 @@ interface BaseView {
fun showMessage(text: String) fun showMessage(text: String)
fun showExpiredDialog() fun showExpiredCredentialsDialog()
fun onCaptchaVerificationRequired(url: String?)
fun showDecryptionFailedDialog()
fun showAuthDialog() fun showAuthDialog()

View File

@ -3,7 +3,8 @@ package io.github.wulkanowy.ui.base
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException 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.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
import io.github.wulkanowy.utils.getErrorString import io.github.wulkanowy.utils.getErrorString
@ -15,7 +16,9 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
var onSessionExpired: () -> Unit = {} var onExpiredCredentials: () -> Unit = {}
var onDecryptionFailed: () -> Unit = {}
var onNoCurrentStudent: () -> Unit = {} var onNoCurrentStudent: () -> Unit = {}
@ -23,24 +26,33 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
var onAuthorizationRequired: () -> Unit = {} var onAuthorizationRequired: () -> Unit = {}
var onCaptchaVerificationRequired: (url: String?) -> Unit = {}
fun dispatch(error: Throwable) { fun dispatch(error: Throwable) {
Timber.e(error, "An exception occurred while the Wulkanowy was running") Timber.e(error, "An exception occurred while the Wulkanowy was running")
proceed(error) proceed(error)
} }
protected open fun proceed(error: Throwable) { protected open fun proceed(error: Throwable) {
showErrorMessage(context.resources.getErrorString(error), error) showDefaultMessage(error)
when (error) { when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException, is BadCredentialsException -> onSessionExpired() is ScramblerException -> onDecryptionFailed()
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent() 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() { open fun clear() {
showErrorMessage = { _, _ -> } showErrorMessage = { _, _ -> }
onSessionExpired = {} onExpiredCredentials = {}
onDecryptionFailed = {}
onNoCurrentStudent = {} onNoCurrentStudent = {}
onPasswordChangeRequired = {} onPasswordChangeRequired = {}
onAuthorizationRequired = {} onAuthorizationRequired = {}

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.attendance package io.github.wulkanowy.ui.modules.attendance
import android.content.res.ColorStateList
import android.graphics.Typeface
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -10,6 +12,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.data.enums.SentExcuseStatus
import io.github.wulkanowy.databinding.ItemAttendanceBinding import io.github.wulkanowy.databinding.ItemAttendanceBinding
import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.isExcusableOrNotExcused import io.github.wulkanowy.utils.isExcusableOrNotExcused
import javax.inject.Inject import javax.inject.Inject
@ -31,15 +34,43 @@ class AttendanceAdapter @Inject constructor() :
) )
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val context = holder.binding.root.context
val item = items[position] val item = items[position]
with(holder.binding) { with(holder.binding) {
attendanceItemNumber.text = item.number.toString() attendanceItemNumber.text = item.number.toString()
attendanceItemSubject.text = item.subject.ifBlank { attendanceItemSubject.text = item.subject
root.context.getString(R.string.all_no_data) .ifBlank { context.getString(R.string.all_no_data) }
}
attendanceItemDescription.setText(item.descriptionRes) attendanceItemDescription.setText(item.descriptionRes)
attendanceItemAlert.isVisible = item.let { it.absence && !it.excused }
attendanceItemDescription.setTextColor(
context.getThemeAttrColor(
when {
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
else -> android.R.attr.textColorSecondary
}
)
)
if (item.exemption || item.excused) {
attendanceItemDescription.setTypeface(null, Typeface.BOLD)
} else {
attendanceItemDescription.setTypeface(null, Typeface.NORMAL)
}
attendanceItemAlert.isVisible =
item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) }
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 attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseInfo.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE
attendanceItemExcuseCheckbox.visibility = View.GONE attendanceItemExcuseCheckbox.visibility = View.GONE
@ -54,10 +85,12 @@ class AttendanceAdapter @Inject constructor() :
attendanceItemExcuseInfo.visibility = View.VISIBLE attendanceItemExcuseInfo.visibility = View.VISIBLE
attendanceItemAlert.visibility = View.INVISIBLE attendanceItemAlert.visibility = View.INVISIBLE
} }
SentExcuseStatus.DENIED -> { SentExcuseStatus.DENIED -> {
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied) attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied)
attendanceItemExcuseInfo.visibility = View.VISIBLE attendanceItemExcuseInfo.visibility = View.VISIBLE
} }
else -> { else -> {
if (item.isExcusableOrNotExcused && excuseActionMode) { if (item.isExcusableOrNotExcused && excuseActionMode) {
attendanceItemNumber.visibility = View.GONE attendanceItemNumber.visibility = View.GONE

View File

@ -6,10 +6,12 @@ import android.view.View
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogAttendanceBinding import io.github.wulkanowy.databinding.DialogAttendanceBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
@ -44,6 +46,16 @@ class AttendanceDialog : BaseDialogFragment<DialogAttendanceBinding>() {
with(binding) { with(binding) {
attendanceDialogSubjectValue.text = attendance.subject attendanceDialogSubjectValue.text = attendance.subject
attendanceDialogDescriptionValue.setText(attendance.descriptionRes) attendanceDialogDescriptionValue.setText(attendance.descriptionRes)
attendanceDialogDescriptionValue.setTextColor(
root.context.getThemeAttrColor(
when {
attendance.absence && !attendance.excused -> R.attr.colorAttendanceAbsence
attendance.lateness && !attendance.excused -> R.attr.colorAttendanceLateness
else -> android.R.attr.textColorSecondary
}
)
)
attendanceDialogDateValue.text = attendance.date.toFormattedString() attendanceDialogDateValue.text = attendance.date.toFormattedString()
attendanceDialogNumberValue.text = attendance.number.toString() attendanceDialogNumberValue.text = attendance.number.toString()
attendanceDialogClose.setOnClickListener { dismiss() } attendanceDialogClose.setOnClickListener { dismiss() }

View File

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

View File

@ -78,4 +78,9 @@ class AuthDialog : BaseDialogFragment<DialogAuthBinding>(), AuthView {
override fun showDescriptionWithName(name: String) { override fun showDescriptionWithName(name: String) {
binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml() 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.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class AuthPresenter @Inject constructor( class AuthPresenter @Inject constructor(
@ -57,13 +58,19 @@ class AuthPresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel) val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
Timber.d("Auth succeed: $isSuccess")
if (isSuccess) { if (isSuccess) {
studentRepository.refreshStudentName(student, semester) studentRepository.refreshStudentAfterAuthorize(student, semester)
} }
isSuccess isSuccess
} }
.onFailure { errorHandler.dispatch(it) } .onFailure {
errorHandler.dispatch(it)
view?.showProgress(false)
view?.showContent(true)
}
.onSuccess { .onSuccess {
Timber.d("Auth fully succeed: $it")
if (it) { if (it) {
view?.showSuccess(true) view?.showSuccess(true)
view?.showContent(false) view?.showContent(false)

View File

@ -0,0 +1,91 @@
package io.github.wulkanowy.ui.modules.captcha
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
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.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
@Inject
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
@Inject
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
private var webView: WebView? = null
companion object {
const val CAPTCHA_SUCCESS = "captcha_success"
private const val CAPTCHA_URL = "captcha_url"
private const val CAPTCHA_CHECK_JS = "document.getElementById('challenge-running') == null"
fun newInstance(url: String?): CaptchaDialog {
return CaptchaDialog().apply {
arguments = bundleOf(CAPTCHA_URL to url)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = DialogCaptchaBinding.inflate(inflater).apply { binding = this }.root
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = false
binding.captchaRefresh.setOnClickListener {
binding.captchaWebview.loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty())
}
binding.captchaClose.setOnClickListener { dismiss() }
with(binding.captchaWebview) {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.create().userAgent
}
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.evaluateJavascript(CAPTCHA_CHECK_JS) {
if (it == "true") {
onChallengeAccepted()
}
}
}
}
loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty())
}
}
private fun onChallengeAccepted() {
runCatching { parentFragmentManager.setFragmentResult(CAPTCHA_SUCCESS, bundleOf()) }
.onFailure { Timber.e(it) }
showMessage(getString(R.string.captcha_verified_message))
dismissAllowingStateLoss()
}
override fun onDestroy() {
webkitCookieManagerProxy.webkitCookieManager?.flush()
webView?.destroy()
super.onDestroy()
}
}

View File

@ -18,8 +18,10 @@ import io.github.wulkanowy.databinding.FragmentDashboardBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog.Companion.CAPTCHA_SUCCESS
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
@ -30,7 +32,12 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getErrorString
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -57,6 +64,9 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
return ((recyclerWidth - margin) / resources.displayMetrics.density).toInt() return ((recyclerWidth - margin) / resources.displayMetrics.density).toInt()
} }
override val isViewEmpty
get() = dashboardAdapter.itemCount == 0
companion object { companion object {
fun newInstance() = DashboardFragment() fun newInstance() = DashboardFragment()
@ -72,6 +82,13 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentDashboardBinding.bind(view) binding = FragmentDashboardBinding.bind(view)
presenter.onAttachView(this) presenter.onAttachView(this)
initializeCaptchaResultObserver()
}
private fun initializeCaptchaResultObserver() {
childFragmentManager.setFragmentResultListener(CAPTCHA_SUCCESS, this) { _, _ ->
presenter.onRetryAfterCaptcha()
}
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -182,8 +199,17 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
binding.dashboardRecycler.isVisible = show binding.dashboardRecycler.isVisible = show
} }
override fun showErrorView(show: Boolean) { override fun showErrorView(show: Boolean, adminMessageItem: DashboardItem.AdminMessages?) {
binding.dashboardErrorContainer.isVisible = show binding.dashboardErrorContainer.isVisible = show
binding.dashboardErrorAdminMessage.root.isVisible = adminMessageItem != null
if (adminMessageItem != null) {
AdminMessageViewHolder(
binding = binding.dashboardErrorAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(adminMessageItem.adminMessage)
}
} }
override fun setErrorDetails(error: Throwable) { override fun setErrorDetails(error: Throwable) {

View File

@ -24,11 +24,13 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.sunday
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -56,6 +58,7 @@ class DashboardPresenter @Inject constructor(
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val attendanceSummaryRepository: AttendanceSummaryRepository, private val attendanceSummaryRepository: AttendanceSummaryRepository,
private val timetableRepository: TimetableRepository, private val timetableRepository: TimetableRepository,
private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase,
private val homeworkRepository: HomeworkRepository, private val homeworkRepository: HomeworkRepository,
private val examRepository: ExamRepository, private val examRepository: ExamRepository,
private val conferenceRepository: ConferenceRepository, private val conferenceRepository: ConferenceRepository,
@ -239,6 +242,14 @@ class DashboardPresenter @Inject constructor(
loadData(selectedDashboardTiles, forceRefresh = true) loadData(selectedDashboardTiles, forceRefresh = true)
} }
fun onRetryAfterCaptcha() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData(selectedDashboardTiles, forceRefresh = true)
}
fun onViewReselected() { fun onViewReselected() {
Timber.i("Dashboard view is reselected") Timber.i("Dashboard view is reselected")
view?.run { view?.run {
@ -293,6 +304,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh forceRefresh = forceRefresh
) )
} }
.mapResourceData { it.map { messageWithAuthor -> messageWithAuthor.message } }
.onResourceError { errorHandler.dispatch(it) } .onResourceError { errorHandler.dispatch(it) }
.takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess .takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
@ -316,7 +328,7 @@ class DashboardPresenter @Inject constructor(
) { luckyNumberResource, messageResource, attendanceResource -> ) { luckyNumberResource, messageResource, attendanceResource ->
val resList = listOf(luckyNumberResource, messageResource, attendanceResource) val resList = listOf(luckyNumberResource, messageResource, attendanceResource)
DashboardItem.HorizontalGroup( resList to DashboardItem.HorizontalGroup(
isLoading = resList.any { it is Resource.Loading }, isLoading = resList.any { it is Resource.Loading },
error = resList.map { it.errorOrNull }.let { errors -> error = resList.map { it.errorOrNull }.let { errors ->
if (errors.all { it != null }) { if (errors.all { it != null }) {
@ -341,9 +353,9 @@ class DashboardPresenter @Inject constructor(
) )
}) })
} }
.filterNot { it.isLoading && forceRefresh } .filterNot { (_, it) -> it.isLoading && forceRefresh }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { .onEach { (_, it) ->
updateData(it, forceRefresh) updateData(it, forceRefresh)
if (it.isLoading) { if (it.isLoading) {
@ -361,7 +373,7 @@ class DashboardPresenter @Inject constructor(
) )
errorHandler.dispatch(it) errorHandler.dispatch(it)
} }
.launch("horizontal_group ${if (forceRefresh) "-forceRefresh" else ""}") .launchWithUniqueRefreshJob("horizontal_group", forceRefresh)
} }
private fun loadGrades(student: Student, forceRefresh: Boolean) { private fun loadGrades(student: Student, forceRefresh: Boolean) {
@ -395,7 +407,7 @@ class DashboardPresenter @Inject constructor(
subjectWithGrades = it.dataOrNull, subjectWithGrades = it.dataOrNull,
gradeTheme = preferencesRepository.gradeColorTheme, gradeTheme = preferencesRepository.gradeColorTheme,
isLoading = true isLoading = true
), forceRefresh ), false
) )
if (!it.dataOrNull.isNullOrEmpty()) { if (!it.dataOrNull.isNullOrEmpty()) {
@ -427,14 +439,17 @@ class DashboardPresenter @Inject constructor(
private fun loadLessons(student: Student, forceRefresh: Boolean) { private fun loadLessons(student: Student, forceRefresh: Boolean) {
flatResourceFlow { flatResourceFlow {
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now() val date = when (isStudentHasLessonsOnWeekendUseCase(semester)) {
true -> LocalDate.now()
else -> LocalDate.now().nextOrSameSchoolDay
}
timetableRepository.getTimetable( timetableRepository.getTimetable(
student = student, student = student,
semester = semester, semester = semester,
start = date, start = date,
end = date.plusDays(1), end = date.sunday,
forceRefresh = forceRefresh forceRefresh = forceRefresh,
) )
} }
.onEach { .onEach {
@ -444,7 +459,7 @@ class DashboardPresenter @Inject constructor(
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData( updateData(
DashboardItem.Lessons(it.dataOrNull, isLoading = true), DashboardItem.Lessons(it.dataOrNull, isLoading = true),
forceRefresh false
) )
if (!it.dataOrNull?.lessons.isNullOrEmpty()) { if (!it.dataOrNull?.lessons.isNullOrEmpty()) {
@ -501,7 +516,7 @@ class DashboardPresenter @Inject constructor(
val data = it.dataOrNull.orEmpty() val data = it.dataOrNull.orEmpty()
updateData( updateData(
DashboardItem.Homework(data, isLoading = true), DashboardItem.Homework(data, isLoading = true),
forceRefresh false
) )
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
@ -535,7 +550,7 @@ class DashboardPresenter @Inject constructor(
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData( updateData(
DashboardItem.Announcements(it.dataOrNull.orEmpty(), isLoading = true), DashboardItem.Announcements(it.dataOrNull.orEmpty(), isLoading = true),
forceRefresh false
) )
if (!it.dataOrNull.isNullOrEmpty()) { if (!it.dataOrNull.isNullOrEmpty()) {
@ -578,7 +593,7 @@ class DashboardPresenter @Inject constructor(
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData( updateData(
DashboardItem.Exams(it.dataOrNull.orEmpty(), isLoading = true), DashboardItem.Exams(it.dataOrNull.orEmpty(), isLoading = true),
forceRefresh false
) )
if (!it.dataOrNull.isNullOrEmpty()) { if (!it.dataOrNull.isNullOrEmpty()) {
@ -619,7 +634,7 @@ class DashboardPresenter @Inject constructor(
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData( updateData(
DashboardItem.Conferences(it.dataOrNull.orEmpty(), isLoading = true), DashboardItem.Conferences(it.dataOrNull.orEmpty(), isLoading = true),
forceRefresh false
) )
if (!it.dataOrNull.isNullOrEmpty()) { if (!it.dataOrNull.isNullOrEmpty()) {
@ -654,7 +669,7 @@ class DashboardPresenter @Inject constructor(
is Resource.Loading -> { is Resource.Loading -> {
Timber.i("Loading dashboard admin message data started") Timber.i("Loading dashboard admin message data started")
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData(DashboardItem.AdminMessages(), forceRefresh) updateData(DashboardItem.AdminMessages(), false)
} }
is Resource.Success -> { is Resource.Success -> {
@ -684,7 +699,7 @@ class DashboardPresenter @Inject constructor(
private fun loadAds(forceRefresh: Boolean) { private fun loadAds(forceRefresh: Boolean) {
presenterScope.launch { presenterScope.launch {
if (!forceRefresh) { if (!forceRefresh) {
updateData(DashboardItem.Ads(), forceRefresh) updateData(DashboardItem.Ads(), false)
} }
val dashboardAdItem = val dashboardAdItem =
@ -805,6 +820,8 @@ class DashboardPresenter @Inject constructor(
val filteredItems = itemsLoadedList.filterNot { val filteredItems = itemsLoadedList.filterNot {
it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE
} }
val dataLoadedAdminMessageItem =
itemsLoadedList.find { it.type == DashboardItem.Type.ADMIN_MESSAGE && it.isDataLoaded } as DashboardItem.AdminMessages?
val isAccountItemError = val isAccountItemError =
itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val isGeneralError = val isGeneralError =
@ -826,7 +843,7 @@ class DashboardPresenter @Inject constructor(
showRefresh(false) showRefresh(false)
if ((forceRefresh && wasGeneralError) || !forceRefresh) { if ((forceRefresh && wasGeneralError) || !forceRefresh) {
showContent(false) showContent(false)
showErrorView(true) showErrorView(true, dataLoadedAdminMessageItem)
setErrorDetails(lastError) setErrorDetails(lastError)
} }
} }
@ -854,6 +871,28 @@ class DashboardPresenter @Inject constructor(
onEach { onEach {
if (it is Resource.Success) { if (it is Resource.Success) {
cancelJobs(jobName) cancelJobs(jobName)
} else if (it is Resource.Error) {
cancelJobs(jobName)
}
}.launch(jobName)
} else {
launch(jobName)
}
}
@JvmName("launchWithUniqueRefreshJobHorizontalGroup")
private fun Flow<Pair<List<Resource<*>>, *>>.launchWithUniqueRefreshJob(
name: String,
forceRefresh: Boolean
) {
val jobName = if (forceRefresh) "$name-forceRefresh" else name
if (forceRefresh) {
onEach { (resources, _) ->
if (resources.all { it is Resource.Success<*> }) {
cancelJobs(jobName)
} else if (resources.any { it is Resource.Error<*> }) {
cancelJobs(jobName)
} }
}.launch(jobName) }.launch(jobName)
} else { } else {

View File

@ -6,6 +6,8 @@ interface DashboardView : BaseView {
val tileWidth: Int val tileWidth: Int
val isViewEmpty: Boolean
fun initView() fun initView()
fun updateData(data: List<DashboardItem>) fun updateData(data: List<DashboardItem>)
@ -18,7 +20,7 @@ interface DashboardView : BaseView {
fun showRefresh(show: Boolean) fun showRefresh(show: Boolean)
fun showErrorView(show: Boolean) fun showErrorView(show: Boolean, adminMessageItem: DashboardItem.AdminMessages? = null)
fun setErrorDetails(error: Throwable) fun setErrorDetails(error: Throwable)

View File

@ -7,7 +7,6 @@ import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
class AdminMessageViewHolder( class AdminMessageViewHolder(
@ -25,9 +24,11 @@ class AdminMessageViewHolder(
context.getThemeAttrColor(R.attr.colorMessageHigh) to context.getThemeAttrColor(R.attr.colorMessageHigh) to
context.getThemeAttrColor(R.attr.colorOnMessageHigh) context.getThemeAttrColor(R.attr.colorOnMessageHigh)
} }
"MEDIUM" -> { "MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
} }
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
} }
@ -37,11 +38,16 @@ class AdminMessageViewHolder(
dashboardAdminMessageItemDescription.text = item.content dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor) dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor) dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible dashboardAdminMessageItemDismiss.isVisible = item.isOkVisible
dashboardAdminMessageItemClose.isVisible = item.isXVisible
dashboardAdminMessageItemDismiss.setTextColor(textColor) dashboardAdminMessageItemDismiss.setTextColor(textColor)
dashboardAdminMessageItemClose.imageTintList = ColorStateList.valueOf(textColor)
dashboardAdminMessageItemDismiss.setOnClickListener { dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item) onAdminMessageDismissClickListener(item)
} }
dashboardAdminMessageItemClose.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url -> item.destinationUrl?.let { url ->

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.debug
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.webkit.CookieManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -58,6 +59,10 @@ class DebugFragment : BaseFragment<FragmentDebugBinding>(R.layout.fragment_debug
(activity as? MainActivity)?.pushView(NotificationDebugFragment.newInstance()) (activity as? MainActivity)?.pushView(NotificationDebugFragment.newInstance())
} }
override fun clearWebkitCookies() {
CookieManager.getInstance().removeAllCookies(null)
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -15,6 +15,7 @@ class DebugPresenter @Inject constructor(
val items = listOf( val items = listOf(
DebugItem(R.string.logviewer_title), DebugItem(R.string.logviewer_title),
DebugItem(R.string.notification_debug_title), DebugItem(R.string.notification_debug_title),
DebugItem(R.string.debug_cookies_clear),
) )
override fun onAttachView(view: DebugView) { override fun onAttachView(view: DebugView) {
@ -31,6 +32,7 @@ class DebugPresenter @Inject constructor(
when (item.title) { when (item.title) {
R.string.logviewer_title -> view?.openLogViewer() R.string.logviewer_title -> view?.openLogViewer()
R.string.notification_debug_title -> view?.openNotificationsDebug() R.string.notification_debug_title -> view?.openNotificationsDebug()
R.string.debug_cookies_clear -> view?.clearWebkitCookies()
else -> Timber.d("Unknown debug item: $item") else -> Timber.d("Unknown debug item: $item")
} }
} }

View File

@ -11,4 +11,6 @@ interface DebugView : BaseView {
fun openLogViewer() fun openLogViewer()
fun openNotificationsDebug() fun openNotificationsDebug()
fun clearWebkitCookies()
} }

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDescriptiveItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeSummaryItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeSummaryItems
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugHomeworkItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugHomeworkItems
@ -55,6 +56,14 @@ class NotificationDebugPresenter @Inject constructor(
NotificationDebugItem(R.string.grade_summary_final_grade) { n -> NotificationDebugItem(R.string.grade_summary_final_grade) { n ->
withStudent { newGradeNotification.notifyFinal(debugGradeSummaryItems.take(n), it) } withStudent { newGradeNotification.notifyFinal(debugGradeSummaryItems.take(n), it) }
}, },
NotificationDebugItem(R.string.grade_summary_descriptive) { n ->
withStudent {
newGradeNotification.notifyDescriptive(
debugGradeDescriptiveItems.take(n),
it
)
}
},
NotificationDebugItem(R.string.homework_title) { n -> NotificationDebugItem(R.string.homework_title) { n ->
withStudent { newHomeworkNotification.notify(debugHomeworkItems.take(n), it) } withStudent { newHomeworkNotification.notify(debugHomeworkItems.take(n), it) }
}, },

View File

@ -0,0 +1,48 @@
package io.github.wulkanowy.ui.modules.debug.notification.mock
import io.github.wulkanowy.data.db.entities.GradeDescriptive
val debugGradeDescriptiveItems = listOf(
generateGradeDescriptive(
"Matematyka",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
),
generateGradeDescriptive("Fizyka", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
generateGradeDescriptive(
"Geografia",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
),
generateGradeDescriptive(
"Sieci komputerowe",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
),
generateGradeDescriptive(
"Systemy operacyjne",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
),
generateGradeDescriptive(
"Język polski",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
),
generateGradeDescriptive(
"Język angielski",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
),
generateGradeDescriptive("Religia", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
generateGradeDescriptive(
"Język niemiecki",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
),
generateGradeDescriptive(
"Wychowanie fizyczne",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
),
)
private fun generateGradeDescriptive(subject: String, description: String) =
GradeDescriptive(
semesterId = 0,
studentId = 0,
subject = subject,
description = description
)

View File

@ -1,15 +1,23 @@
package io.github.wulkanowy.ui.modules.grade package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeDescriptive
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.mapData
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.* import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER
import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier import io.github.wulkanowy.utils.changeModifier
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -62,6 +70,7 @@ class GradeAverageProvider @Inject constructor(
forceRefresh = forceRefresh, forceRefresh = forceRefresh,
params = params, params = params,
) )
BOTH_SEMESTERS -> calculateCombinedAverage( BOTH_SEMESTERS -> calculateCombinedAverage(
student = student, student = student,
semesters = semesters, semesters = semesters,
@ -69,6 +78,7 @@ class GradeAverageProvider @Inject constructor(
forceRefresh = forceRefresh, forceRefresh = forceRefresh,
config = params, config = params,
) )
ALL_YEAR -> calculateCombinedAverage( ALL_YEAR -> calculateCombinedAverage(
student = student, student = student,
semesters = semesters, semesters = semesters,
@ -149,7 +159,7 @@ class GradeAverageProvider @Inject constructor(
?.updateModifiers(student, config).orEmpty() ?.updateModifiers(student, config).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
config.isOptionalArithmeticAverage isOptionalArithmeticAverage = config.isOptionalArithmeticAverage,
) )
} else { } else {
secondSemesterSubject.average secondSemesterSubject.average
@ -163,13 +173,21 @@ class GradeAverageProvider @Inject constructor(
config: AverageCalcParams, config: AverageCalcParams,
): Double { ): Double {
return if (!isAnyVulcanAverage || config.forceAverageCalc) { 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 val secondSemesterAverage = secondSemesterSubject.grades
.updateModifiers(student, config) .updateModifiers(student, config)
.calcAverage(config.isOptionalArithmeticAverage) .calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
val firstSemesterAverage = firstSemesterSubject?.grades val firstSemesterAverage = firstSemesterSubject?.grades
?.updateModifiers(student, config) ?.updateModifiers(student, config)
?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage ?.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
?: secondSemesterAverage
(secondSemesterAverage + firstSemesterAverage) / divider (secondSemesterAverage + firstSemesterAverage) / divider
} else { } else {
@ -189,27 +207,38 @@ class GradeAverageProvider @Inject constructor(
): Flow<Resource<List<GradeSubject>>> { ): Flow<Resource<List<GradeSubject>>> {
return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh) return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh)
.mapResourceData { res -> .mapResourceData { res ->
val (details, summaries) = res val (details, summaries, descriptives) = res
val isAnyAverage = summaries.any { it.average != .0 } val isAnyAverage = summaries.any { it.average != .0 }
val allGrades = details.groupBy { it.subject } val allGrades = details.groupBy { it.subject }
val descriptiveGradesBySubject = descriptives.associateBy { it.subject }
val items = summaries.emulateEmptySummaries( val items = summaries
.createEmptySummariesByGradesIfNeeded(
student = student, student = student,
semester = semester, semester = semester,
grades = allGrades.toList(), grades = allGrades.toList(),
calcAverage = isAnyAverage, calcAverage = isAnyAverage,
params = params, params = params,
).map { summary -> )
.createEmptySummariesByDescriptiveGradesIfNeeded(
student = student,
semester = semester,
descriptives = descriptives,
)
.map { summary ->
val grades = allGrades[summary.subject].orEmpty() val grades = allGrades[summary.subject].orEmpty()
val descriptiveGrade = descriptiveGradesBySubject[summary.subject]
GradeSubject( GradeSubject(
subject = summary.subject, subject = summary.subject,
average = if (!isAnyAverage || params.forceAverageCalc) { average = if (!isAnyAverage || params.forceAverageCalc) {
grades.updateModifiers(student, params) grades.updateModifiers(student, params)
.calcAverage(params.isOptionalArithmeticAverage) .calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
} else summary.average, } else summary.average,
points = summary.pointsSum, points = summary.pointsSum,
summary = summary, summary = summary,
grades = grades, grades = grades,
descriptive = descriptiveGrade,
isVulcanAverage = isAnyAverage isVulcanAverage = isAnyAverage
) )
} }
@ -218,7 +247,33 @@ class GradeAverageProvider @Inject constructor(
} }
} }
private fun List<GradeSummary>.emulateEmptySummaries( private fun List<GradeSummary>.createEmptySummariesByDescriptiveGradesIfNeeded(
student: Student,
semester: Semester,
descriptives: List<GradeDescriptive>
): List<GradeSummary> {
val summarySubjects = this.map { it.subject }
val gradeSummaryToAdd = descriptives.mapNotNull { gradeDescriptive ->
if (gradeDescriptive.subject in summarySubjects) return@mapNotNull null
GradeSummary(
studentId = student.studentId,
semesterId = semester.semesterId,
position = 0,
subject = gradeDescriptive.subject,
predictedGrade = "",
finalGrade = "",
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = .0
)
}
return this + gradeSummaryToAdd
}
private fun List<GradeSummary>.createEmptySummariesByGradesIfNeeded(
student: Student, student: Student,
semester: Semester, semester: Semester,
grades: List<Pair<String, List<Grade>>>, grades: List<Pair<String, List<Grade>>>,
@ -239,8 +294,13 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "", proposedPoints = "",
finalPoints = "", finalPoints = "",
pointsSum = "", pointsSum = "",
average = if (calcAverage) details.updateModifiers(student, params) average = when {
.calcAverage(params.isOptionalArithmeticAverage) else .0 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
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -31,14 +30,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
@Inject @Inject
lateinit var presenter: GradePresenter lateinit var presenter: GradePresenter
private val pagerAdapter by lazy {
BaseFragmentPagerAdapter(
fragmentManager = childFragmentManager,
pagesCount = 3,
lifecycle = lifecycle,
)
}
private var semesterSwitchMenu: MenuItem? = null private var semesterSwitchMenu: MenuItem? = null
companion object { companion object {
@ -52,6 +43,8 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem override val currentPageIndex get() = binding.gradeViewPager.currentItem
private var pagerAdapter: BaseFragmentPagerAdapter? = null
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -71,13 +64,26 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
} }
override fun initView() { 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) { with(binding.gradeViewPager) {
adapter = pagerAdapter adapter = pagerAdapter
offscreenPageLimit = 3 offscreenPageLimit = 3
setOnSelectPageListener(presenter::onPageSelected) setOnSelectPageListener(presenter::onPageSelected)
} }
with(pagerAdapter) { with(pagerAdapter!!) {
containerId = binding.gradeViewPager.id containerId = binding.gradeViewPager.id
titleFactory = { titleFactory = {
when (it) { when (it) {
@ -99,11 +105,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
} }
binding.gradeTabLayout.elevation = requireContext().dpToPx(4f) binding.gradeTabLayout.elevation = requireContext().dpToPx(4f)
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { 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) { override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView) (pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)
?.onParentLoadData(semesterId, forceRefresh) ?.onParentLoadData(semesterId, forceRefresh)
} }
override fun notifyChildParentReselected(index: Int) { override fun notifyChildParentReselected(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected() (pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
} }
override fun notifyChildSemesterChange(index: Int) { override fun notifyChildSemesterChange(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester() (pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
} }
override fun onDestroyView() { override fun onDestroyView() {
pagerAdapter = null
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()
} }

View File

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

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.grade package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeDescriptive
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
data class GradeSubject( data class GradeSubject(
@ -8,6 +9,7 @@ data class GradeSubject(
val average: Double, val average: Double,
val points: String, val points: String,
val summary: GradeSummary, val summary: GradeSummary,
val descriptive: GradeDescriptive?,
val grades: List<Grade>, val grades: List<Grade>,
val isVulcanAverage: Boolean val isVulcanAverage: Boolean
) )

View File

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

View File

@ -1,13 +1,22 @@
package io.github.wulkanowy.ui.modules.grade.details package io.github.wulkanowy.ui.modules.grade.details
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.enums.GradeExpandMode import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode.* import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC
import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE
import io.github.wulkanowy.data.enums.GradeSortingMode.DATE
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.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
@ -207,20 +216,20 @@ class GradeDetailsPresenter @Inject constructor(
AVERAGE -> gradeSubjects.sortedByDescending { it.average } AVERAGE -> gradeSubjects.sortedByDescending { it.average }
} }
} }
.map { (subject, average, points, _, grades) -> .map { gradeSubject ->
val subItems = grades val subItems = gradeSubject.grades
.sortedByDescending { it.date } .sortedByDescending { it.date }
.map { GradeDetailsItem(it, ViewType.ITEM) } .map { GradeDetailsItem(it, ViewType.ITEM) }
val gradeDetailsItems = listOf( val gradeDetailsItems = listOf(
GradeDetailsItem( GradeDetailsItem(
GradeDetailsHeader( GradeDetailsHeader(
subject = subject, subject = gradeSubject.subject,
average = average, average = gradeSubject.average,
pointsSum = points, pointsSum = gradeSubject.points,
grades = subItems grades = subItems
).apply { ).apply {
newGrades = grades.filter { grade -> !grade.isRead }.size newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size
}, ViewType.HEADER }, ViewType.HEADER
) )
) )

View File

@ -2,16 +2,16 @@ package io.github.wulkanowy.ui.modules.grade.summary
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemGradeSummaryBinding import io.github.wulkanowy.databinding.ItemGradeSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding
import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid
import io.github.wulkanowy.utils.calcFinalAverage import io.github.wulkanowy.utils.calcFinalAverage
import io.github.wulkanowy.utils.ifNullOrBlank
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -24,7 +24,7 @@ class GradeSummaryAdapter @Inject constructor(
ITEM(2) ITEM(2)
} }
var items = emptyList<GradeSummary>() var items = emptyList<GradeSummaryItem>()
var onCalculatedHelpClickListener: () -> Unit = {} var onCalculatedHelpClickListener: () -> Unit = {}
@ -44,9 +44,11 @@ class GradeSummaryAdapter @Inject constructor(
ViewType.HEADER.id -> HeaderViewHolder( ViewType.HEADER.id -> HeaderViewHolder(
ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false) ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false)
) )
ViewType.ITEM.id -> ItemViewHolder( ViewType.ITEM.id -> ItemViewHolder(
ItemGradeSummaryBinding.inflate(inflater, parent, false) ItemGradeSummaryBinding.inflate(inflater, parent, false)
) )
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
@ -60,19 +62,23 @@ class GradeSummaryAdapter @Inject constructor(
private fun bindHeaderViewHolder(binding: ScrollableHeaderGradeSummaryBinding) { private fun bindHeaderViewHolder(binding: ScrollableHeaderGradeSummaryBinding) {
if (items.isEmpty()) return if (items.isEmpty()) return
val gradeSummaries = items
.filter { it.gradeDescriptive == null }
.map { it.gradeSummary }
val context = binding.root.context val context = binding.root.context
val finalItemsCount = items.count { isGradeValid(it.finalGrade) } val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) }
val calculatedItemsCount = items.count { value -> value.average != 0.0 } val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) }
val finalAverage = items.calcFinalAverage( val finalAverage = gradeSummaries.calcFinalAverage(
preferencesRepository.gradePlusModifier, preferencesRepository.gradePlusModifier,
preferencesRepository.gradeMinusModifier preferencesRepository.gradeMinusModifier
) )
val calculatedAverage = items.filter { value -> value.average != 0.0 } val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 }
.map { values -> values.average } .map { values -> values.average }
.reversed() // fix average precision .reversed() // fix average precision
.average() .average()
.let { if (it.isNaN()) 0.0 else it }
with(binding) { with(binding) {
gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage) gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage)
@ -95,16 +101,28 @@ class GradeSummaryAdapter @Inject constructor(
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummary) { private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummaryItem) {
with(binding) { val (gradeSummary, gradeDescriptive) = item
gradeSummaryItemTitle.text = item.subject
gradeSummaryItemPoints.text = item.pointsSum
gradeSummaryItemAverage.text = formatAverage(item.average, "")
gradeSummaryItemPredicted.text = "${item.predictedGrade} ${item.proposedPoints}".trim()
gradeSummaryItemFinal.text = "${item.finalGrade} ${item.finalPoints}".trim()
gradeSummaryItemPointsContainer.visibility = with(binding) {
if (item.pointsSum.isBlank()) View.GONE else View.VISIBLE gradeSummaryItemTitle.text = gradeSummary.subject
gradeSummaryItemPoints.text = gradeSummary.pointsSum
gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "")
gradeSummaryItemPredicted.text =
"${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim()
gradeSummaryItemFinal.text =
"${gradeSummary.finalGrade} ${gradeSummary.finalPoints}".trim()
gradeSummaryItemDescriptive.text = gradeDescriptive?.description.ifNullOrBlank {
root.context.getString(R.string.all_no_data)
}
gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPredictedContainer.isVisible = gradeDescriptive == null
gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null
gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null
gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank()
} }
} }

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