Compare commits

..

143 Commits

Author SHA1 Message Date
359432d24d [4.6-beta.1] Update build.gradle, signing and changelog. 2021-02-25 19:54:19 +01:00
edf8ec20f0 [Proguard] Add rules for FSLogin. 2021-02-25 19:53:41 +01:00
3ad9e5da1f [Vulcan/Web] Update web login to work with FSLogin Realms. 2021-02-25 19:29:06 +01:00
459bbf78b2 [Vulcan/Hebe] Add getting attachments in homework. 2021-02-24 23:45:27 +01:00
d0baf02750 [Vulcan/Hebe] Remove login method dependency. 2021-02-24 22:09:51 +01:00
a5bb7d9c6e [Lab] Add option to full sync and clear profile. 2021-02-24 21:53:59 +01:00
a939d95ea3 [Vulcan/Hebe] Add getting attachments in messages. 2021-02-24 21:18:35 +01:00
4c081c970e [Vulcan/Hebe] Exclude more endpoints from first semester sync. 2021-02-23 12:50:37 +01:00
b75ab76c2a [Vulcan/Web] Fix checking login expiry time. 2021-02-23 12:48:17 +01:00
9da6dbccb3 [Vulcan/Web] Fix registering mobile device. 2021-02-23 12:47:52 +01:00
66444ae35b [Vulcan/Web] Fix extracting permissions string. 2021-02-23 12:47:20 +01:00
3e8b3de2b7 [Vulcan/Web] Move web data to loginStore per-symbol. 2021-02-23 12:00:01 +01:00
85ac5769a1 [4.5.1-beta.1] Update build.gradle, signing and changelog. 2021-02-22 23:25:44 +01:00
93e3d5994a [Vulcan/Hebe] Restore sending date range when getting attendance. 2021-02-22 23:20:32 +01:00
0cf24c527b [Vulcan/Hebe] Fix logging tag in notices class. 2021-02-22 23:04:10 +01:00
97c5acd6ba [Vulcan/Hebe] Fix checking for current year. 2021-02-22 22:59:02 +01:00
30aeb70647 [Vulcan/Hebe] Add filtering data by current school year. 2021-02-22 22:54:13 +01:00
b599d679c4 [Vulcan/Hebe] Implement sending messages. 2021-02-22 22:43:36 +01:00
1eecd24d91 [Vulcan/Hebe] Add getting notices. 2021-02-22 22:40:28 +01:00
c698dfdb73 [Vulcan/Hebe] Add getting grade summary. 2021-02-22 21:33:42 +01:00
c7a44f5ced [Vulcan/Hebe] Implement push notifications. 2021-02-22 19:13:25 +01:00
c8ee6ff1e7 [Vulcan/Hebe] Add getting lucky number. 2021-02-22 18:49:53 +01:00
552acd4043 [Vulcan/Web] Fix web login. 2021-02-22 18:23:27 +01:00
ede101ea20 [Vulcan/Hebe] Add saving lesson topic in attendance. 2021-02-22 17:51:52 +01:00
13c2640ed5 [UI] Hide API deprecation message on other profiles. 2021-02-22 17:43:34 +01:00
dd0739fd4b Merge branch 'develop' into feature/vulcan-hebe 2021-02-22 17:36:48 +01:00
9023f13932 [Vulcan/Hebe] Add missing copyright to files. 2021-02-22 17:18:40 +01:00
f8456fb087 [Lab] Add archiver enabled checkbox. 2021-02-22 16:37:53 +01:00
9b48041cd9 [4.5] Update build.gradle, signing and changelog. 2021-02-22 00:03:52 +01:00
46cecf3474 Merge branch 'feature/vulcan-hebe' into develop 2021-02-21 23:57:25 +01:00
c27254bcad [Vulcan] Remove API login mode form. 2021-02-21 23:55:11 +01:00
80333cdea4 [Vulcan] Add API deprecation message. 2021-02-21 23:52:45 +01:00
6aee3ea420 [4.5-beta.2] Update build.gradle, signing and changelog. 2021-02-21 23:36:55 +01:00
a11a44b768 [Vulcan/Hebe] Add handling custom presence types. 2021-02-21 23:30:20 +01:00
e869107101 [Vulcan/Hebe] Add syncing both semesters during first sync. 2021-02-21 23:26:43 +01:00
5903bbe59d [Vulcan/Hebe] Fix getting attendance. 2021-02-21 23:03:06 +01:00
6c0ddd3e6d [Vulcan/Hebe] Add setting message status as read. 2021-02-21 22:55:26 +01:00
621a7ac642 [Vulcan/Hebe] Add getting attendance. 2021-02-21 22:28:54 +01:00
e86b47fb1b [Vulcan/Hebe] Add getting messages. 2021-02-21 21:50:44 +01:00
98fb7ac8c9 [Vulcan/Web] Fix Proguard rules for platform selection. 2021-02-21 19:15:53 +01:00
f49e39e858 [Vulcan/Hebe] Add getting teacher list and addressbook. 2021-02-21 19:13:47 +01:00
8fc57cd3f5 [Vulcan/Hebe] Fix getting classroom name. 2021-02-21 17:22:21 +01:00
a9eda087e0 [Extensions] Update Gson extensions to check type. 2021-02-21 17:20:13 +01:00
3f36a284ee [4.5-beta.1] Update build.gradle, signing and changelog. 2021-02-21 16:40:40 +01:00
1814fd67e1 [Strings] Update copyright date. 2021-02-21 16:39:02 +01:00
54e49af943 [Vulcan/Hebe] Add getting timetable and lesson changes. 2021-02-21 16:18:33 +01:00
d6a67a0da6 [Vulcan/Hebe] Add getting homework list. 2021-02-21 12:18:53 +01:00
28725c6400 [Vulcan/Hebe] Add getting exams. 2021-02-21 00:27:42 +01:00
4fc965d970 [Vulcan/Hebe] Add saving unit ID. 2021-02-20 23:52:40 +01:00
2aaf713d58 [Vulcan/Hebe] Add next sync and data removing in grades. 2021-02-20 23:45:27 +01:00
c7d2ac4e3e [Vulcan/Hebe] Add getting grades. 2021-02-20 23:07:23 +01:00
ae20c30c88 [Vulcan/Hebe] Fix Firebase token for registration. 2021-02-20 21:31:52 +01:00
aef3f66654 [Vulcan/Hebe] Add API list helper. Separate student list code. 2021-02-20 20:11:54 +01:00
c7abde8f11 [Vulcan/Hebe] Update login mode requirement. 2021-02-20 18:51:08 +01:00
2fcff33bd6 [Vulcan/Hebe] Add hebe API login implementation. 2021-02-19 13:37:31 +01:00
b08e4c2d3d [Gradle] Update OkHttp to 3.12.13. 2021-02-19 13:24:12 +01:00
73ff09052c [Gradle] Bump target SDK to 30. 2021-02-17 14:43:00 +01:00
9649afd43f [Gradle] Update libraries and dependencies. 2021-02-17 14:42:08 +01:00
ed3a245b51 [UI/Login] Add new easter eggs. 2020-10-18 22:14:41 +02:00
477730708f [UI/Login] Add refresh button in platform list. 2020-10-17 23:44:17 +02:00
f39d0c595d [UI/Login] Add recommended, testing and dev only badges to login modes. 2020-10-17 23:10:07 +02:00
46407f9647 [4.4.3] Update build.gradle, signing and changelog. 2020-10-16 23:54:48 +02:00
6ecb97b87e [Login/Podlasie] Add option to logout other devices. 2020-10-16 23:50:44 +02:00
ecdaaeae65 [API/Vulcan] Add KO1 routing rule. 2020-10-16 18:06:34 +02:00
a0c302b663 [App] Swap devMode with debugMode. Fix hiding sticks from old. 2020-10-16 17:18:19 +02:00
b31039ecd9 [API/Mobidziennik] Fix getting recipient list. 2020-10-16 16:57:00 +02:00
5c84086f42 [Settings/Grades] Add hiding sticks from old. 2020-10-14 22:26:47 +02:00
752cdfa8d6 Implement wear module base. 2020-09-17 18:05:17 +02:00
8e3d404352 [4.4.2] Update build.gradle, signing and changelog. 2020-09-05 19:14:11 +02:00
810cfd8092 [API] Rename response parameters to fix compatibility. 2020-09-05 19:13:37 +02:00
bd2a9524c6 [UI] Use HtmlCompat instead of Html. Fix a typo. 2020-09-05 18:47:30 +02:00
d780d5118d [Proguard] Add rules to fix API responses. 2020-09-05 18:38:53 +02:00
f1570b8eb9 [4.4.1] Update build.gradle, signing and changelog. 2020-09-04 15:44:26 +02:00
de0f29a09e [API/Mobidziennik] Fix getting attendance when a day has no lessons. 2020-09-04 00:04:23 +02:00
c0d11c91e3 [API/Mobidziennik] Fix trimming subject name in timetable. 2020-09-03 23:51:56 +02:00
22c540a3d4 [UI] Improve register unavailable dialog and card. 2020-09-03 23:50:53 +02:00
b7e35d0322 [4.4] Update build.gradle, signing and changelog. 2020-09-03 14:08:54 +02:00
7bcd6bf038 [Sync] Implement checking register availability. Improve app updates. 2020-09-03 13:39:46 +02:00
ea4591144b [4.3.1] Update build.gradle, signing and changelog. 2020-08-29 00:00:38 +02:00
7627d184a2 [API/Librus] Update client parameters. 2020-08-28 23:47:28 +02:00
076b485fda [API] Enable back sync before school year. 2020-08-28 23:27:34 +02:00
09cb97e367 [4.3] Update build.gradle, signing and changelog. 2020-08-28 15:26:42 +02:00
4e1f2ed41a [UI] Update date in about card subtext. Make gradlew executable. 2020-08-27 12:07:07 +02:00
281b6a95ef [API] Fix for syncing new profiles after archiving. 2020-08-27 00:10:19 +02:00
e40871c0d0 [UI] Update register names, again. 2020-08-26 22:15:48 +02:00
b74eeed994 [UI] Update register names. 2020-08-26 21:36:30 +02:00
ccde482364 [UI] Update register names. 2020-08-25 23:48:51 +02:00
a02033d0f3 [API] Fix archiving compatibility for older app versions. 2020-08-25 22:25:21 +02:00
6c6bc89f57 [UI] Improve archive-related UI. Add archived info home card. 2020-08-25 19:14:11 +02:00
1e3da45340 [UI] Remove unused home cards. 2020-08-25 17:02:12 +02:00
0d366adddb [UI] Implement showing archived profiles in drawer. 2020-08-25 16:01:11 +02:00
2c24eba46d [UI] Show bottom bar badge in debug versions. 2020-08-25 12:05:58 +02:00
7c6dbca986 [API] Implement basic profile archiving. 2020-08-25 10:46:50 +02:00
33a8fa2a1e [API] Fix doubled sync on API error. 2020-08-24 18:56:26 +02:00
300e2c4bc2 [API/Librus] Fix doubled sync on reCaptcha timeout. 2020-08-24 18:16:47 +02:00
f883318bd2 [Gradle] Fix compilation issues in latest Android Studio. 2020-08-24 17:42:53 +02:00
5460c1e2a0 [4.2.1] Update build.gradle, signing and changelog. 2020-05-21 23:12:25 +02:00
137c975e81 [API/Vulcan] Add getting Firebase token from server. 2020-05-21 22:07:31 +02:00
001de4a88c [Firebase] Fix getting FCM tokens and try to fix Vulcan registering. 2020-05-20 22:04:39 +02:00
5dcb3fd580 [Data] Fix setting correct time zone in ISO date parsing. 2020-05-18 12:22:21 +02:00
f13995aa5c [API/Mobidziennik] Fix lucky number extraction. 2020-05-18 11:42:58 +02:00
e23deb5ca6 [API/Podlasie] Fix security token generation. 2020-05-18 11:41:27 +02:00
d688b379a2 [4.2] Update build.gradle, signing and changelog. 2020-05-16 21:27:56 +02:00
d6a796e25e [Login] Crop Podlasie logo. Add Feedback activity, remove help icon. 2020-05-16 20:59:28 +02:00
e02d3e571d [API/Librus] Fix some more captcha errors. 2020-05-16 20:52:16 +02:00
907b75b22d [Proguard] Add rule for app login platform. 2020-05-16 20:46:54 +02:00
c3660b5f80 [Login] Change Librus logo. Disable Synergia & Vulcan e-mail login. 2020-05-16 20:44:56 +02:00
7ff10df70c [Login] Fix not showing sync fragment. 2020-05-16 20:33:37 +02:00
83e1b21ec3 [API/Podlasie] Add downloading attachments in homework. 2020-05-14 18:54:37 +02:00
deb54e4b24 [API/Podlasie] Add getting homework. 2020-05-14 12:06:52 +02:00
48873caecc [DB/Grades] Fix DataRemoveModel deleting models instead of marking as don't keep. 2020-05-14 11:43:22 +02:00
cadd1a3dbd Merge branch 'feature/prymus' into develop 2020-05-13 23:16:34 +02:00
f09f069b2c [API/Podlasie] Move event description to homework body. 2020-05-13 22:56:22 +02:00
1fb5aaed5d [UI/Podlasie] Show homework fragment in drawer. 2020-05-13 22:55:45 +02:00
65ba330d5f [API/Podlasie] Fix encoding in events topic. 2020-05-13 22:49:01 +02:00
795317f13f [API/Podlasie] Add getting teachers. 2020-05-13 22:47:05 +02:00
031cc05209 [API/Podlasie] Add getting events. 2020-05-13 22:17:50 +02:00
0ac8e1d9c1 [API/Podlasie] Add getting the lucky number. 2020-05-13 19:56:41 +02:00
4389dc9d79 [API/Podlasie] Add getting grades proposals. 2020-05-13 19:37:24 +02:00
b13257cb78 [API/Podlasie] Add getting grades. 2020-05-13 17:33:42 +02:00
fcffa2afeb [API/Podlasie] Fix saving class team, add main endpoint and getting the timetable. 2020-05-13 16:56:42 +02:00
3c2f85f263 Merge branch 'develop' into feature/prymus 2020-05-12 20:37:15 +02:00
0a2323acf3 [API/Podlasie] Implement first login and login page. 2020-05-12 20:25:45 +02:00
45c2948ed1 [Login] Fix not showing errors after one successful login. 2020-05-12 18:23:29 +02:00
f72a6103b5 Merge branch 'feature/new-login' into develop 2020-05-12 18:20:08 +02:00
9261848369 [API] Improve Lab fragment. Fix OkHttp crashing on API <21. 2020-05-12 13:41:40 +02:00
7f4e45c57c Merge branch 'develop' into feature/prymus 2020-05-11 21:06:18 +02:00
180154b684 Merge branch 'new-login' into develop 2020-05-11 20:59:38 +02:00
a4f58eb19b [API/Podlasie] Implement basic Podlasie e-register support. 2020-05-11 20:41:13 +02:00
fada483d55 [Login] Add missing e-registers. 2020-05-11 20:37:25 +02:00
3ae9ba3d61 Merge branch 'develop' into feature/new-login
# Conflicts:
#	app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginApi.kt
#	app/src/main/res/values/strings.xml
2020-05-11 19:06:07 +02:00
15102fe818 [Hotfix] Suppress ConvertSecondaryConstructorToPrimary in LibrusLoginApi. 2020-05-11 18:14:15 +02:00
8864bb2a5e [UI] Add new app icon and splash logo. 2020-05-11 13:42:30 +02:00
ef1cdd5b20 [API/Librus] Use Synergia message module when messages module login fails. 2020-05-10 19:44:44 +02:00
35f4f34342 [API/Vulcan] Add syncing first semester. Disable counting releases in attendance. 2020-05-09 23:01:42 +02:00
1a8134459a [API/Librus] Implement downloading messages using Synergia endpoints. 2020-05-09 20:11:09 +02:00
a6ce3a5068 [API/Librus] Make downloading attachments use Synergia. 2020-05-09 20:11:09 +02:00
328b20f78b [Fix] Increase HTTP client timeout duration. 2020-05-09 20:11:09 +02:00
771712da99 [UI/Attendance] Fix counting issues. Add attendance details dialog. 2020-05-09 19:18:18 +02:00
f685a4dceb [API/Vulcan] Implement Vulcan lucky numbers. 2020-04-22 20:05:36 +02:00
e8dad29e5d Merge branch 'develop' into feature/new-login 2020-04-20 19:05:05 +02:00
27e49b10fd [API] Implement draft Vulcan Web login. 2020-04-19 19:27:27 +02:00
97dc8d12f1 [Login] Add new login user interface. 2020-04-16 11:01:53 +02:00
333 changed files with 11367 additions and 5946 deletions

View File

@ -1,6 +1,10 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
@ -11,7 +15,6 @@
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -22,7 +25,6 @@
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -34,7 +36,6 @@
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -45,7 +46,6 @@
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -56,7 +56,6 @@
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -67,7 +66,6 @@
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -78,7 +76,6 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -90,7 +87,6 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -102,7 +98,6 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
@ -112,5 +107,8 @@
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

17
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.7">
<module name="annotation" target="1.7" />
<module name="codegen" target="1.7" />
<module name="Szkolny.eu.agendacalendarview" target="11" />
<module name="Szkolny.eu.app" target="11" />
<module name="Szkolny.eu.cafebar" target="11" />
<module name="Szkolny.eu.material-about-library" target="11" />
<module name="Szkolny.eu.mhttp" target="11" />
<module name="Szkolny.eu.nachos" target="11" />
<module name="Szkolny.eu.szkolny-font" target="11" />
<module name="Szkolny.eu.wear" target="11" />
</bytecodeTargetLevel>
</component>
</project>

2
.idea/discord.xml generated
View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="true" />
<option name="show" value="PROJECT_FILES" />
</component>
<component name="ProjectNotificationSettings">
<option name="askShowProject" value="false" />

3
.idea/misc.xml generated
View File

@ -11,7 +11,6 @@
<item index="1" class="java.lang.String" itemvalue="org.greenrobot.eventbus.Subscribe" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.RecentlyNonNull" />
@ -51,7 +50,7 @@
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -3,6 +3,7 @@
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />

View File

@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
apply plugin: 'com.google.firebase.crashlytics'
android {
signingConfigs {
@ -54,10 +54,11 @@ android {
lintOptions {
checkReleaseBuilds false
}
dataBinding {
enabled = true
buildFeatures {
dataBinding = true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility '1.8'
targetCompatibility '1.8'
}
@ -75,6 +76,7 @@ android {
version "3.10.2"
}
}
ndkVersion '21.3.6528147'
}
/*task finalizeBundleDebug(type: Copy) {
@ -104,6 +106,8 @@ tasks.whenTaskAdded { task ->
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
kapt "androidx.room:room-compiler:${versions.room}"
debugImplementation "com.amitshekhar.android:debug-db:1.0.5"
@ -114,7 +118,7 @@ dependencies {
implementation "androidx.core:core-ktx:${versions.ktx}"
implementation "androidx.gridlayout:gridlayout:${versions.gridLayout}"
implementation "androidx.legacy:legacy-support-v4:${versions.legacy}"
implementation "androidx.lifecycle:lifecycle-livedata:${versions.lifecycle}"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycle}"
implementation "androidx.recyclerview:recyclerview:${versions.recyclerView}"
implementation "androidx.room:room-runtime:${versions.room}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
@ -137,7 +141,7 @@ dependencies {
implementation "cat.ereza:customactivityoncrash:2.2.0"
implementation "com.applandeo:material-calendar-view:1.5.0"
implementation "com.crashlytics.sdk.android:crashlytics:2.10.1"
implementation 'com.google.firebase:firebase-crashlytics:17.3.1'
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.evernote:android-job:1.2.6"
implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2"
@ -145,7 +149,11 @@ dependencies {
implementation("com.github.ozodrukh:CircularReveal:2.0.1@aar") {transitive = true}
implementation "com.heinrichreimersoftware:material-intro:1.5.8" // do not update
implementation "com.jaredrummler:colorpicker:1.0.2"
implementation "com.squareup.okhttp3:okhttp:3.12.2"
implementation("com.squareup.okhttp3:okhttp") {
version {
strictly "3.12.13"
}
}
implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0" // do not update
implementation "com.wdullaer:materialdatetimepicker:4.1.2"
implementation "com.yuyh.json:jsonviewer:1.0.6"
@ -153,7 +161,7 @@ dependencies {
implementation "me.grantland:autofittextview:0.2.1"
implementation "me.leolin:ShortcutBadger:1.1.22@aar"
implementation "org.greenrobot:eventbus:3.1.1"
implementation "org.jsoup:jsoup:1.10.1"
implementation "org.jsoup:jsoup:1.12.1"
implementation "pl.droidsonroids.gif:android-gif-drawable:1.2.15"
//implementation "se.emilsjolander:stickylistheaders:2.7.0"
implementation 'com.github.edisonw:StickyListHeaders:master-SNAPSHOT@aar'
@ -176,6 +184,7 @@ dependencies {
//implementation "org.redundent:kotlin-xml-builder:1.5.3"
implementation "io.github.wulkanowy:signer-android:0.1.1"
implementation 'com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31'
implementation "androidx.work:work-runtime-ktx:${versions.work}"
@ -199,8 +208,13 @@ dependencies {
implementation 'com.google.android:flexbox:2.0.1'
implementation 'com.qifan.powerpermission:powerpermission:1.0.0'
implementation 'com.qifan.powerpermission:powerpermission-coroutines:1.0.0'
implementation 'com.qifan.powerpermission:powerpermission:1.3.0'
implementation 'com.qifan.powerpermission:powerpermission-coroutines:1.3.0'
implementation 'com.github.kuba2k2.FSLogin:lib:2.0.0'
implementation 'pl.droidsonroids:jspoon:1.3.2'
implementation "com.squareup.retrofit2:converter-scalars:2.8.1"
implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2"
}
repositories {
mavenCentral()

View File

@ -64,5 +64,9 @@
-keep class pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing { public final byte[] pleaseStopRightNow(java.lang.String, long); }
-keepclassmembernames class pl.szczodrzynski.edziennik.data.api.szkolny.request.** { *; }
-keepclassmembernames class pl.szczodrzynski.edziennik.data.api.szkolny.response.** { *; }
-keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.request.** { *; }
-keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.response.** { *; }
-keepclassmembernames class pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo$Platform { *; }
-keepclassmembernames class pl.szczodrzynski.fslogin.realm.RealmData { *; }
-keepclassmembernames class pl.szczodrzynski.fslogin.realm.RealmData$Type { *; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -24,7 +24,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme.Dark"
android:usesCleartextTraffic="true"
@ -135,9 +134,6 @@
android:configChanges="orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/AppTheme.Light" />
<activity android:name=".ui.modules.login.LoginLibrusCaptchaActivity"
android:theme="@android:style/Theme.Dialog"
android:excludeFromRecents="true"/>
<activity android:name=".ui.modules.home.CounterActivity"
android:theme="@style/AppTheme.Black" />
<activity android:name=".ui.modules.feedback.FeedbackActivity"

View File

@ -1,10 +1,10 @@
<h3>Wersja 4.1, 2020-05-09</h3>
<h3>Wersja 4.6-beta.1, 2021-02-25</h3>
<ul>
<li>Naprawiona synchronizacja Wiadomości w Librusie.</li>
<li>Widok adresu dołączenia do lekcji online w kalendarzu (jeżeli nauczyciel wpisze adres).</li>
<li>Nowy moduł Frekwencji, obsługujący również lekcje zdalne.</li>
<li>Vulcan: dodano możliwość logowania adresem e-mail lub nazwą użytkownika.</li>
<li>Vulcan: dodano obsługę załączników w wiadomościach i zadaniach domowych.</li>
<li>Zalecane jest zalogowanie się ponownie.</li>
</ul>
<br>
<br>
Dzięki za korzystanie ze Szkolnego!<br>
<i>&copy; Kuba Szczodrzyński, Kacper Ziubryniewicz 2020</i>
<i>&copy; Kuba Szczodrzyński, Kacper Ziubryniewicz 2021</i>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0xec, 0xc0, 0x3e, 0x4e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0xe5, 0x07, 0x0f, 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -43,6 +43,7 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity
import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.managers.*
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
@ -56,8 +57,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val profileId
get() = profile.id
var devMode = false
var debugMode = false
var devMode = false
}
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
@ -101,12 +102,12 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
.followSslRedirects(true)
.retryOnConnectionFailure(true)
.cookieJar(cookieJar)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
builder.installHttpsSupport(this)
if (debugMode || BuildConfig.DEBUG) {
if (devMode || BuildConfig.DEBUG) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
@ -161,7 +162,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
Iconics.registerFont(SzkolnyFont)
App.db = AppDb(this)
Themes.themeInt = config.ui.theme
debugMode = config.debugMode
devMode = config.debugMode
MHttp.instance().customOkHttpClient(http)
if (!profileLoadById(config.lastProfileId)) {
@ -172,7 +173,9 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
setLanguage(it)
}
devMode = BuildConfig.DEBUG
debugMode = BuildConfig.DEBUG
if (BuildConfig.DEBUG)
devMode = true
Signing.getCert(this)
@ -182,7 +185,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
if (config.devModePassword != null)
checkDevModePassword()
debugMode = devMode || config.debugMode
devMode = debugMode || config.debugMode
if (config.sync.enabled)
SyncWorker.scheduleNext(this@App, false)
@ -255,6 +258,10 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val pushMobidziennikApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setProjectId("mobidziennik")
.setStorageBucket("mobidziennik.appspot.com")
.setDatabaseUrl("https://mobidziennik.firebaseio.com")
.setGcmSenderId("747285019373")
.setApiKey("AIzaSyCi5LmsZ5BBCQnGtrdvWnp1bWLCNP8OWQE")
.setApplicationId("1:747285019373:android:f6341bf7b158621d")
.build(),
@ -264,6 +271,10 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val pushLibrusApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setProjectId("synergiadru")
.setStorageBucket("synergiadru.appspot.com")
.setDatabaseUrl("https://synergiadru.firebaseio.com")
.setGcmSenderId("513056078587")
.setApiKey("AIzaSyDfTuEoYPKdv4aceEws1CO3n0-HvTndz-o")
.setApplicationId("1:513056078587:android:1e29083b760af544")
.build(),
@ -273,19 +284,38 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val pushVulcanApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setProjectId("dzienniczekplus")
.setStorageBucket("dzienniczekplus.appspot.com")
.setDatabaseUrl("https://dzienniczekplus.firebaseio.com")
.setGcmSenderId("987828170337")
.setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA")
.setApplicationId("1:987828170337:android:ac97431a0a4578c3")
.build(),
"Vulcan"
)
val pushVulcanHebeApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setProjectId("dzienniczekplus")
.setStorageBucket("dzienniczekplus.appspot.com")
.setDatabaseUrl("https://dzienniczekplus.firebaseio.com")
.setGcmSenderId("987828170337")
.setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA")
.setApplicationId("1:987828170337:android:7e16404b9e5deaaa")
.build(),
"VulcanHebe"
)
try {
FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
d("Firebase", "Got App token: $token")
config.sync.tokenApp = token
}
FirebaseInstanceId.getInstance(pushMobidziennikApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
d("Firebase", "Got Mobidziennik2 token: $token")
if (token != config.sync.tokenMobidziennik) {
config.sync.tokenMobidziennik = token
config.sync.tokenMobidziennikList = listOf()
@ -293,6 +323,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
}
FirebaseInstanceId.getInstance(pushLibrusApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
d("Firebase", "Got Librus token: $token")
if (token != config.sync.tokenLibrus) {
config.sync.tokenLibrus = token
config.sync.tokenLibrusList = listOf()
@ -300,11 +331,20 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
}
FirebaseInstanceId.getInstance(pushVulcanApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
d("Firebase", "Got Vulcan token: $token")
if (token != config.sync.tokenVulcan) {
config.sync.tokenVulcan = token
config.sync.tokenVulcanList = listOf()
}
}
FirebaseInstanceId.getInstance(pushVulcanHebeApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
d("Firebase", "Got VulcanHebe token: $token")
if (token != config.sync.tokenVulcanHebe) {
config.sync.tokenVulcanHebe = token
config.sync.tokenVulcanHebeList = listOf()
}
}
FirebaseMessaging.getInstance().subscribeToTopic(packageName)
} catch (e: IllegalStateException) {
e.printStackTrace()
@ -345,6 +385,9 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
if (!success) {
EventBus.getDefault().post(ProfileListEmptyEvent())
}
else {
onSuccess(profile)
}
}
}
fun profileSave() = profileSave(profile)

View File

@ -42,10 +42,10 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewpager.widget.ViewPager
import com.google.android.gms.security.ProviderInstaller
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.gson.*
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import im.wangchao.mhttp.Response
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@ -95,30 +95,30 @@ fun List<Teacher>.byNameFDotSpaceLast(nameFDotSpaceLast: String) = firstOrNull {
fun JsonObject?.get(key: String): JsonElement? = this?.get(key)
fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (it.isJsonNull) null else it.asBoolean }
fun JsonObject?.getString(key: String): String? = get(key)?.let { if (it.isJsonNull) null else it.asString }
fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (it.isJsonNull) null else it.asInt }
fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (it.isJsonNull) null else it.asLong }
fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(it.isJsonNull) null else it.asFloat }
fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(it.isJsonNull) null else it.asCharacter }
fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean }
fun JsonObject?.getString(key: String): String? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asString }
fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt }
fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong }
fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat }
fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asCharacter }
fun JsonObject?.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonObject) it.asJsonObject else null }
fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonArray) it.asJsonArray else null }
fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (it.isJsonNull) defaultValue else it.asBoolean } ?: defaultValue
fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (it.isJsonNull) defaultValue else it.asString } ?: defaultValue
fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (it.isJsonNull) defaultValue else it.asInt } ?: defaultValue
fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (it.isJsonNull) defaultValue else it.asLong } ?: defaultValue
fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(it.isJsonNull) defaultValue else it.asFloat } ?: defaultValue
fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(it.isJsonNull) defaultValue else it.asCharacter } ?: defaultValue
fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asBoolean } ?: defaultValue
fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asString } ?: defaultValue
fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asInt } ?: defaultValue
fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asLong } ?: defaultValue
fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asFloat } ?: defaultValue
fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asCharacter } ?: defaultValue
fun JsonObject?.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonObject) it.asJsonObject else defaultValue } ?: defaultValue
fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonArray) it.asJsonArray else defaultValue } ?: defaultValue
fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asBoolean }
fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asString }
fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asInt }
fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asLong }
fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(it.isJsonNull) null else it.asFloat }
fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(it.isJsonNull) null else it.asCharacter }
fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean }
fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asString }
fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt }
fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong }
fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat }
fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asCharacter }
fun JsonArray.getJsonObject(key: Int): JsonObject? = if (key >= size()) null else get(key)?.let { if (it.isJsonObject) it.asJsonObject else null }
fun JsonArray.getJsonArray(key: Int): JsonArray? = if (key >= size()) null else get(key)?.let { if (it.isJsonArray) it.asJsonArray else null }
@ -298,7 +298,7 @@ fun colorFromCssName(name: String): Int {
"orange" -> 0xffffa500
"black" -> 0xff000000
"white" -> 0xffffffff
else -> -1
else -> -1L
}.toInt()
}
@ -537,6 +537,12 @@ fun String.md5(): String {
return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0')
}
fun String.sha1Hex(): String {
val md = MessageDigest.getInstance("SHA-1")
md.update(toByteArray())
return md.digest().joinToString("") { "%02x".format(it) }
}
fun String.sha256(): ByteArray {
val md = MessageDigest.getInstance("SHA-256")
md.update(toByteArray())
@ -672,6 +678,16 @@ fun TextView.setText(@StringRes resid: Int, vararg formatArgs: Any) {
text = context.getString(resid, *formatArgs)
}
fun MaterialAlertDialogBuilder.setTitle(@StringRes resid: Int, vararg formatArgs: Any): MaterialAlertDialogBuilder {
setTitle(context.getString(resid, *formatArgs))
return this
}
fun MaterialAlertDialogBuilder.setMessage(@StringRes resid: Int, vararg formatArgs: Any): MaterialAlertDialogBuilder {
setMessage(context.getString(resid, *formatArgs))
return this
}
fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
return JsonObject().apply {
for (property in properties) {
@ -686,6 +702,21 @@ fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
}
}
fun JsonObject.toBundle(): Bundle {
return Bundle().also {
for ((key, value) in this.entrySet()) {
when (value) {
is JsonObject -> it.putBundle(key, value.toBundle())
is JsonPrimitive -> when {
value.isString -> it.putString(key, value.asString)
value.isBoolean -> it.putBoolean(key, value.asBoolean)
value.isNumber -> it.putInt(key, value.asInt)
}
}
}
}
}
fun JsonArray(vararg properties: Any?): JsonArray {
return JsonArray().apply {
for (property in properties) {
@ -1161,7 +1192,7 @@ fun Iterable<Float>.averageOrNull() = this.average().let { if (it.isNaN()) null
fun String.copyToClipboard(context: Context) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("Tekst", this)
clipboard.primaryClip = clipData
clipboard.setPrimaryClip(clipData)
}
fun TextView.getTextPosition(range: IntRange): Rect {
@ -1245,3 +1276,5 @@ val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener
operator fun <K, V> Iterable<Pair<K, V>>.get(key: K): V? {
return firstOrNull { it.first == key }?.second
}
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }

View File

@ -38,16 +38,22 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
@ -59,6 +65,7 @@ import pl.szczodrzynski.edziennik.ui.modules.base.MainSnackbar
import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment
import pl.szczodrzynski.edziennik.ui.modules.debug.DebugFragment
import pl.szczodrzynski.edziennik.ui.modules.debug.LabFragment
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment
import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment
@ -227,7 +234,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class).withPopTo(DRAWER_ITEM_MESSAGES)
list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class)
list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class)
if (App.debugMode) {
if (App.devMode) {
list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class)
list += NavTarget(TARGET_LAB, R.string.menu_lab, LabFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_flask_outline)
@ -295,12 +302,21 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
mainSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
errorSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
if (BuildConfig.VERSION_NAME.contains("nightly")) {
b.nightlyText.isVisible = true
b.nightlyText.text = "Nightly\n"+BuildConfig.VERSION_NAME.substringAfterLast(".")
when {
BuildConfig.VERSION_NAME.contains("nightly") -> {
b.nightlyText.isVisible = true
b.nightlyText.text = "Nightly\n"+BuildConfig.VERSION_NAME.substringAfterLast(".")
}
BuildConfig.VERSION_NAME.contains("daily") -> {
b.nightlyText.isVisible = true
b.nightlyText.text = "Daily\n"+BuildConfig.VERSION_NAME.substringAfterLast(".")
}
BuildConfig.DEBUG -> {
b.nightlyText.isVisible = true
b.nightlyText.text = "Debug\n"+BuildConfig.VERSION_NAME
}
else -> b.nightlyText.isVisible = false
}
else
b.nightlyText.isVisible = false
navLoading = true
@ -399,7 +415,20 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
app.db.profileDao().all.observe(this, Observer { profiles ->
drawer.setProfileList(profiles.filter { it.id >= 0 }.toMutableList())
val allArchived = profiles.all { it.archived }
drawer.setProfileList(profiles.filter { it.id >= 0 && (!it.archived || allArchived) }.toMutableList())
//prepend the archived profile if loaded
if (app.profile.archived && !allArchived) {
drawer.prependProfile(Profile(
id = app.profile.id,
loginStoreId = app.profile.loginStoreId,
loginStoreType = app.profile.loginStoreType,
name = app.profile.name,
subname = "Archiwum - ${app.profile.subname}"
).also {
it.archived = true
})
}
drawer.currentProfile = App.profileId
})
@ -415,7 +444,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
})
b.swipeRefreshLayout.isEnabled = true
b.swipeRefreshLayout.setOnRefreshListener { this.syncCurrentFeature() }
b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } }
b.swipeRefreshLayout.setColorSchemeResources(
R.color.md_blue_500,
R.color.md_amber_500,
@ -425,6 +454,23 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
SyncWorker.scheduleNext(app)
UpdateWorker.scheduleNext(app)
// if loaded profile is archived, switch to the up-to-date version of it
if (app.profile.archived) {
launch {
if (app.profile.archiveId != null) {
val profile = withContext(Dispatchers.IO) {
app.db.profileDao().getNotArchivedOf(app.profile.archiveId!!)
}
if (profile != null)
loadProfile(profile)
else
loadProfile(0)
} else {
loadProfile(0)
}
}
}
// APP BACKGROUND
if (app.config.ui.appBackground != null) {
try {
@ -522,7 +568,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline)
.withOnClickListener(View.OnClickListener { loadTarget(TARGET_FEEDBACK) })
)
if (App.debugMode) {
if (App.devMode) {
bottomSheet += BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_debug)
.withIcon(CommunityMaterial.Icon.cmd_android_studio)
@ -564,7 +610,66 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|_____/ \__, |_| |_|\___|
__/ |
|__*/
fun syncCurrentFeature() {
suspend fun syncCurrentFeature() {
if (app.profile.archived) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.profile_archived_title)
.setMessage(
R.string.profile_archived_text,
app.profile.studentSchoolYearStart,
app.profile.studentSchoolYearStart + 1
)
.setPositiveButton(R.string.ok, null)
.show()
swipeRefreshLayout.isRefreshing = false
return
}
if (app.profile.shouldArchive()) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.profile_archiving_title)
.setMessage(
R.string.profile_archiving_format,
app.profile.dateYearEnd.formattedString
)
.setPositiveButton(R.string.ok, null)
.show()
}
if (app.profile.isBeforeYear()) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.profile_year_not_started_title)
.setMessage(
R.string.profile_year_not_started_format,
app.profile.dateSemester1Start.formattedString
)
.setPositiveButton(R.string.ok, null)
.show()
swipeRefreshLayout.isRefreshing = false
return
}
app.profile.registerName?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheckAt < currentTimeUnix()) {
withContext(Dispatchers.IO) {
val api = SzkolnyApi(app)
api.runCatching(this@MainActivity) {
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
status = availability[registerName]
}
}
}
if (status?.available != true
|| status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) {
swipeRefreshLayout.isRefreshing = false
loadTarget(DRAWER_ITEM_HOME)
if (status != null)
RegisterUnavailableDialog(this, status!!)
return
}
}
swipeRefreshLayout.isRefreshing = true
Toast.makeText(this, fragmentToSyncName(navTargetId), Toast.LENGTH_SHORT).show()
val fragmentParam = when (navTargetId) {
@ -581,6 +686,20 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
arguments = arguments
).enqueue(this)
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onUpdateEvent(event: Update) {
EventBus.getDefault().removeStickyEvent(event)
UpdateAvailableDialog(this, event)
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event)
app.profile.registerName?.let { registerName ->
event.data[registerName]?.let {
RegisterUnavailableDialog(this, it)
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) {
swipeRefreshLayout.isRefreshing = true
@ -632,6 +751,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) {
EventBus.getDefault().removeStickyEvent(event)
if (event.error.errorCode == ERROR_VULCAN_API_DEPRECATED) {
if (event.error.profileId != App.profileId)
return
ErrorDetailsDialog(this, listOf(event.error))
}
navView.toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
@ -885,23 +1009,51 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
fun loadProfile(id: Int) = loadProfile(id, navTargetId)
fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments)
fun loadProfile(id: Int, drawerSelection: Int, arguments: Bundle? = null) {
fun loadProfile(profile: Profile) = loadProfile(
profile,
navTargetId,
null,
if (app.profile.archived) app.profile.id else null
)
private fun loadProfile(id: Int, drawerSelection: Int, arguments: Bundle? = null) {
if (App.profileId == id) {
drawer.currentProfile = app.profile.id
loadTarget(drawerSelection, arguments)
return
}
val previousArchivedId = if (app.profile.archived) app.profile.id else null
app.profileLoad(id) {
MessagesFragment.pageSelection = -1
setDrawerItems()
// the drawer profile is updated automatically when the drawer item is clicked
// update it manually when switching profiles from other source
//if (drawer.currentProfile != app.profile.id)
drawer.currentProfile = app.profileId
loadTarget(drawerSelection, arguments)
loadProfile(it, drawerSelection, arguments, previousArchivedId)
}
}
private fun loadProfile(profile: Profile, drawerSelection: Int, arguments: Bundle?, previousArchivedId: Int?) {
App.profile = profile
MessagesFragment.pageSelection = -1
setDrawerItems()
if (previousArchivedId != null) {
// prevents accidentally removing the first item if the archived profile is not shown
drawer.removeProfileById(previousArchivedId)
}
if (profile.archived) {
drawer.prependProfile(Profile(
id = profile.id,
loginStoreId = profile.loginStoreId,
loginStoreType = profile.loginStoreType,
name = profile.name,
subname = "Archiwum - ${profile.subname}"
).also {
it.archived = true
})
}
// the drawer profile is updated automatically when the drawer item is clicked
// update it manually when switching profiles from other source
//if (drawer.currentProfile != app.profile.id)
drawer.currentProfile = app.profileId
loadTarget(drawerSelection, arguments)
}
fun loadTarget(id: Int, arguments: Bundle? = null) {
var loadId = id
if (loadId == -1) {

View File

@ -105,6 +105,11 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() }
set(value) { set("widgetConfigs", value); mWidgetConfigs = value }
private var mArchiverEnabled: Boolean? = null
var archiverEnabled: Boolean
get() { mArchiverEnabled = mArchiverEnabled ?: values.get("archiverEnabled", true); return mArchiverEnabled ?: true }
set(value) { set("archiverEnabled", value); mArchiverEnabled = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init {

View File

@ -4,12 +4,18 @@
package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.setMap
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigSync(private val config: Config) {
private val gson = Gson()
private var mDontShowAppManagerDialog: Boolean? = null
var dontShowAppManagerDialog: Boolean
get() { mDontShowAppManagerDialog = mDontShowAppManagerDialog ?: config.values.get("dontShowAppManagerDialog", false); return mDontShowAppManagerDialog ?: false }
@ -93,6 +99,10 @@ class ConfigSync(private val config: Config) {
var tokenVulcan: String?
get() { mTokenVulcan = mTokenVulcan ?: config.values.get("tokenVulcan", null as String?); return mTokenVulcan }
set(value) { config.set("tokenVulcan", value); mTokenVulcan = value }
private var mTokenVulcanHebe: String? = null
var tokenVulcanHebe: String?
get() { mTokenVulcanHebe = mTokenVulcanHebe ?: config.values.get("tokenVulcanHebe", null as String?); return mTokenVulcanHebe }
set(value) { config.set("tokenVulcanHebe", value); mTokenVulcanHebe = value }
private var mTokenMobidziennikList: List<Int>? = null
var tokenMobidziennikList: List<Int>
@ -106,4 +116,13 @@ class ConfigSync(private val config: Config) {
var tokenVulcanList: List<Int>
get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() }
set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value }
private var mTokenVulcanHebeList: List<Int>? = null
var tokenVulcanHebeList: List<Int>
get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() }
set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value }
private var mRegisterAvailability: Map<String, RegisterAvailabilityStatus>? = null
var registerAvailability: Map<String, RegisterAvailabilityStatus>
get() { mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> gson.fromJson<Map<String, RegisterAvailabilityStatus>>(it, object: TypeToken<Map<String, RegisterAvailabilityStatus>>(){}.type) }; return mRegisterAvailability ?: mapOf() }
set(value) { config.setMap("registerAvailability", value); mRegisterAvailability = value }
}

View File

@ -49,4 +49,9 @@ class ProfileConfigGrades(private val config: ProfileConfig) {
var dontCountGrades: List<String>
get() { mDontCountGrades = mDontCountGrades ?: config.values.get("dontCountGrades", listOf()); return mDontCountGrades ?: listOf() }
set(value) { config.set("dontCountGrades", value); mDontCountGrades = value }
private var mHideSticksFromOld: Boolean? = null
var hideSticksFromOld: Boolean
get() { mHideSticksFromOld = mHideSticksFromOld ?: config.values.get("hideSticksFromOld", false); return mHideSticksFromOld ?: false }
set(value) { config.set("hideSticksFromOld", value); mHideSticksFromOld = value }
}

View File

@ -49,6 +49,9 @@ fun AbstractConfig.setIntList(key: String, value: List<Int>?) {
fun AbstractConfig.setLongList(key: String, value: List<Long>?) {
set(key, value?.let { gson.toJson(it) })
}
fun <K, V> AbstractConfig.setMap(key: String, value: Map<K, V>?) {
set(key, value?.let { gson.toJson(it) })
}
fun HashMap<String, String?>.get(key: String, default: String?): String? {
return this[key] ?: default

View File

@ -24,14 +24,14 @@ const val FAKE_LIBRUS_ACCOUNTS = "/synergia_accounts.php"
val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp"
const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"
const val LIBRUS_CLIENT_ID = "6XPsKf10LPz1nxgHQLcvZ1KM48DYzlBAhxipaXY8"
const val LIBRUS_REDIRECT_URL = "http://localhost/bar"
const val LIBRUS_CLIENT_ID = "0RbsDOkV9tyKEQYzlLv5hs3DM1ukrynFI4p6C1Yc"
const val LIBRUS_REDIRECT_URL = "app://librus"
const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code"
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action"
const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"
const val LIBRUS_ACCOUNT_URL = "/v2/SynergiaAccounts/fresh/" // + login
const val LIBRUS_ACCOUNTS_URL = "/v2/SynergiaAccounts"
const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login
const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts"
/** https://api.librus.pl/2.0 */
const val LIBRUS_API_URL = "https://api.librus.pl/2.0"
@ -57,6 +57,7 @@ const val LIBRUS_MESSAGES_URL = "https://wiadomosci.librus.pl/module"
const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
const val IDZIENNIK_USER_AGENT = SYNERGIA_USER_AGENT
const val IDZIENNIK_WEB_URL = "https://iuczniowie.progman.pl/idziennik"
@ -91,10 +92,19 @@ val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
const val VULCAN_API_USER_AGENT = "MobileUserAgent"
const val VULCAN_API_APP_NAME = "VULCAN-Android-ModulUcznia"
const val VULCAN_API_APP_VERSION = "19.4.1.436"
const val VULCAN_API_APP_VERSION = "20.5.1.470"
const val VULCAN_API_PASSWORD = "CE75EA598C7743AD9B0B7328DED85B06"
const val VULCAN_API_PASSWORD_FAKELOG = "012345678901234567890123456789AB"
val VULCAN_API_DEVICE_NAME = "Szkolny.eu ${Build.MODEL}"
const val VULCAN_HEBE_USER_AGENT = "Dart/2.10 (dart:io)"
const val VULCAN_HEBE_APP_NAME = "DzienniczekPlus 2.0"
const val VULCAN_HEBE_APP_VERSION = "21.02.09 (G)"
private const val VULCAN_API_DEVICE_NAME_PREFIX = "Szkolny.eu "
private const val VULCAN_API_DEVICE_NAME_SUFFIX = " - nie usuwać"
val VULCAN_API_DEVICE_NAME by lazy {
val base = "$VULCAN_API_DEVICE_NAME_PREFIX${Build.MODEL}"
val baseMaxLength = 50 - VULCAN_API_DEVICE_NAME_SUFFIX.length
base.take(baseMaxLength) + VULCAN_API_DEVICE_NAME_SUFFIX
}
const val VULCAN_API_ENDPOINT_CERTIFICATE = "mobile-api/Uczen.v3.UczenStart/Certyfikat"
const val VULCAN_API_ENDPOINT_STUDENT_LIST = "mobile-api/Uczen.v3.UczenStart/ListaUczniow"
@ -113,5 +123,28 @@ const val VULCAN_API_ENDPOINT_MESSAGES_ADD = "mobile-api/Uczen.v3.Uczen/DodajWia
const val VULCAN_API_ENDPOINT_PUSH = "mobile-api/Uczen.v3.Uczen/UstawPushToken"
const val VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/WiadomosciZalacznik"
const val VULCAN_API_ENDPOINT_HOMEWORK_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/ZadaniaDomoweZalacznik"
const val VULCAN_WEB_ENDPOINT_LUCKY_NUMBER = "Start.mvc/GetKidsLuckyNumbers"
const val VULCAN_WEB_ENDPOINT_REGISTER_DEVICE = "RejestracjaUrzadzeniaToken.mvc/Get"
const val VULCAN_HEBE_ENDPOINT_REGISTER_NEW = "api/mobile/register/new"
const val VULCAN_HEBE_ENDPOINT_MAIN = "api/mobile/register/hebe"
const val VULCAN_HEBE_ENDPOINT_PUSH_ALL = "api/mobile/push/all"
const val VULCAN_HEBE_ENDPOINT_TIMETABLE = "api/mobile/schedule"
const val VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES = "api/mobile/schedule/changes"
const val VULCAN_HEBE_ENDPOINT_ADDRESSBOOK = "api/mobile/addressbook"
const val VULCAN_HEBE_ENDPOINT_EXAMS = "api/mobile/exam"
const val VULCAN_HEBE_ENDPOINT_GRADES = "api/mobile/grade"
const val VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY = "api/mobile/grade/summary"
const val VULCAN_HEBE_ENDPOINT_HOMEWORK = "api/mobile/homework"
const val VULCAN_HEBE_ENDPOINT_NOTICES = "api/mobile/note"
const val VULCAN_HEBE_ENDPOINT_ATTENDANCE = "api/mobile/lesson"
const val VULCAN_HEBE_ENDPOINT_MESSAGES = "api/mobile/message"
const val VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS = "api/mobile/message/status"
const val VULCAN_HEBE_ENDPOINT_MESSAGES_SEND = "api/mobile/message"
const val VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER = "api/mobile/school/lucky"
const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}"
const val PODLASIE_API_VERSION = "1.0.62"
const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api"
const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia"
const val PODLASIE_API_LOGOUT_DEVICES_ENDPOINT = "/wyczyscUrzadzenia"

View File

@ -160,6 +160,18 @@ const val ERROR_VULCAN_API_MAINTENANCE = 340
const val ERROR_VULCAN_API_BAD_REQUEST = 341
const val ERROR_VULCAN_API_OTHER = 342
const val ERROR_VULCAN_ATTACHMENT_DOWNLOAD = 343
const val ERROR_VULCAN_WEB_DATA_MISSING = 344
const val ERROR_VULCAN_WEB_429 = 345
const val ERROR_VULCAN_WEB_OTHER = 346
const val ERROR_VULCAN_WEB_NO_CERTIFICATE = 347
const val ERROR_VULCAN_WEB_NO_REGISTER = 348
const val ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED = 349
const val ERROR_VULCAN_WEB_LOGGED_OUT = 350
const val ERROR_VULCAN_WEB_CERTIFICATE_POST_FAILED = 351
const val ERROR_VULCAN_WEB_GRADUATE_ACCOUNT = 352
const val ERROR_VULCAN_WEB_NO_SCHOOLS = 353
const val ERROR_VULCAN_HEBE_OTHER = 354
const val ERROR_VULCAN_API_DEPRECATED = 390
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402
@ -190,6 +202,12 @@ const val ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS = 521
const val ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED = 522
const val ERROR_EDUDZIENNIK_WEB_TEAM_MISSING = 530
const val ERROR_LOGIN_PODLASIE_API_INVALID_TOKEN = 601
const val ERROR_LOGIN_PODLASIE_API_DEVICE_LIMIT = 602
const val ERROR_PODLASIE_API_NO_TOKEN = 630
const val ERROR_PODLASIE_API_OTHER = 631
const val ERROR_PODLASIE_API_DATA_MISSING = 632
const val ERROR_TEMPLATE_WEB_OTHER = 801
const val EXCEPTION_API_TASK = 900
@ -210,5 +228,9 @@ const val EXCEPTION_IDZIENNIK_API_REQUEST = 914
const val EXCEPTION_EDUDZIENNIK_WEB_REQUEST = 920
const val EXCEPTION_EDUDZIENNIK_FILE_REQUEST = 921
const val ERROR_ONEDRIVE_DOWNLOAD = 930
const val EXCEPTION_VULCAN_WEB_LOGIN = 931
const val EXCEPTION_VULCAN_WEB_REQUEST = 932
const val EXCEPTION_PODLASIE_API_REQUEST = 940
const val EXCEPTION_VULCAN_HEBE_REQUEST = 950
const val LOGIN_NO_ARGUMENTS = 1201

View File

@ -13,9 +13,12 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginPor
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginSynergia
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginApi2
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginWeb
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginWeb
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
// librus
@ -27,7 +30,6 @@ import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
const val SYNERGIA_API_ENABLED = false
const val LOGIN_TYPE_IDZIENNIK = 3
const val LOGIN_TYPE_TEMPLATE = 21
@ -97,17 +99,19 @@ val mobidziennikLoginMethods = listOf(
const val LOGIN_TYPE_VULCAN = 4
const val LOGIN_MODE_VULCAN_API = 0
const val LOGIN_MODE_VULCAN_WEB = 1
const val LOGIN_MODE_VULCAN_HEBE = 2
const val LOGIN_METHOD_VULCAN_WEB_MAIN = 100
const val LOGIN_METHOD_VULCAN_WEB_NEW = 200
const val LOGIN_METHOD_VULCAN_WEB_OLD = 300
const val LOGIN_METHOD_VULCAN_WEB_MESSAGES = 400
const val LOGIN_METHOD_VULCAN_API = 500
const val LOGIN_METHOD_VULCAN_HEBE = 600
val vulcanLoginMethods = listOf(
/*LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java)
.withIsPossible { _, _ -> false }
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java)
.withIsPossible { _, loginStore -> loginStore.hasLoginData("webHost") }
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED },
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_NEW, VulcanLoginWebNew::class.java)
/*LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_NEW, VulcanLoginWebNew::class.java)
.withIsPossible { _, _ -> false }
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN },
@ -116,10 +120,16 @@ val vulcanLoginMethods = listOf(
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN },*/
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_API, VulcanLoginApi::class.java)
.withIsPossible { _, _ -> true }
.withRequiredLoginMethod { _, loginStore ->
if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_NEW else LOGIN_METHOD_NOT_NEEDED
.withIsPossible { _, loginStore ->
loginStore.mode == LOGIN_MODE_VULCAN_API
}
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED },
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_HEBE, VulcanLoginHebe::class.java)
.withIsPossible { _, loginStore ->
loginStore.mode != LOGIN_MODE_VULCAN_API
}
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
)
val idziennikLoginMethods = listOf(
@ -133,6 +143,7 @@ val idziennikLoginMethods = listOf(
)
const val LOGIN_TYPE_EDUDZIENNIK = 5
const val LOGIN_MODE_EDUDZIENNIK_WEB = 0
const val LOGIN_METHOD_EDUDZIENNIK_WEB = 100
val edudziennikLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_EDUDZIENNIK, LOGIN_METHOD_EDUDZIENNIK_WEB, EdudziennikLoginWeb::class.java)
@ -140,6 +151,15 @@ val edudziennikLoginMethods = listOf(
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
)
const val LOGIN_TYPE_PODLASIE = 6
const val LOGIN_MODE_PODLASIE_API = 0
const val LOGIN_METHOD_PODLASIE_API = 100
val podlasieLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_PODLASIE, LOGIN_METHOD_PODLASIE_API, PodlasieLoginApi::class.java)
.withIsPossible { _, _ -> true }
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
)
val templateLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_WEB, TemplateLoginWeb::class.java)
.withIsPossible { _, _ -> true }

View File

@ -12,6 +12,10 @@ object Regexes {
"""color: (\w+);?""".toRegex()
}
val NOT_DIGITS by lazy {
"""[^0-9]""".toRegex()
}
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {
@ -40,7 +44,7 @@ object Regexes {
"""\(([0-9A-ząęóżźńśłć]*?)\)$""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_LUCKY_NUMBER by lazy {
"""class="szczesliwy_numerek".*>0*([0-9]+)(?:/0*[0-9]+)*</a>""".toRegex(DOT_MATCHES_ALL)
"""class="szczesliwy_numerek".*?>0?([0-9]+)/?0?([0-9]+)?</a>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_CLASS_CALENDAR by lazy {
"""events: (.+),$""".toRegex(RegexOption.MULTILINE)
@ -80,6 +84,15 @@ object Regexes {
val MOBIDZIENNIK_ATTENDANCE_ENTRIES by lazy {
"""font-size:.+?class=".*?">(.*?)</td>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_ATTENDANCE_COLUMNS by lazy {
"""<tr><td class="border-right1".+?/td>(.+?)</tr>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_ATTENDANCE_COLUMN by lazy {
"""(<td.+?>)(.*?)</td>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_ATTENDANCE_COLUMN_SPAN by lazy {
"""colspan="(\d+)"""".toRegex()
}
val MOBIDZIENNIK_ATTENDANCE_RANGE by lazy {
"""<span>([0-9:]+) - .+? (.+?)</span></a>""".toRegex(DOT_MATCHES_ALL)
}
@ -142,12 +155,21 @@ object Regexes {
val VULCAN_SHIFT_ANNOTATION by lazy {
"""\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex()
}
val VULCAN_WEB_PERMISSIONS by lazy {
"""permissions: '([A-z0-9/=+\-_|]+?)'""".toRegex()
}
val VULCAN_WEB_SYMBOL_VALIDATE by lazy {
"""[A-z0-9]+""".toRegex(IGNORE_CASE)
}
val LIBRUS_ATTACHMENT_KEY by lazy {
"""singleUseKey=([0-9A-z_]+)""".toRegex()
}
val LIBRUS_MESSAGE_ID by lazy {
"""/wiadomosci/[0-9]+/[0-9]+/([0-9]+?)/""".toRegex()
}

View File

@ -5,29 +5,37 @@
package pl.szczodrzynski.edziennik.data.api.edziennik
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.Edudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.Idziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan
import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d
open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) {
companion object {
private const val TAG = "EdziennikTask"
var profile: Profile? = null
var loginStore: LoginStore? = null
fun firstLogin(loginStore: LoginStore) = EdziennikTask(-1, FirstLoginRequest(loginStore))
fun sync() = EdziennikTask(-1, SyncRequest())
fun syncProfile(profileId: Int, viewIds: List<Pair<Int, Int>>? = null, onlyEndpoints: List<Int>? = null, arguments: JsonObject? = null) = EdziennikTask(profileId, SyncProfileRequest(viewIds, onlyEndpoints, arguments))
@ -59,21 +67,65 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
// save the profile ID and name as the current task's
taskName = app.getString(R.string.edziennik_notification_api_sync_title_format, profile.name)
}
EdziennikTask.profile = this.profile
EdziennikTask.loginStore = this.loginStore
}
private var edziennikInterface: EdziennikInterface? = null
internal fun run(app: App, taskCallback: EdziennikCallback) {
if (profile?.archived == true) {
taskCallback.onError(ApiError(TAG, ERROR_PROFILE_ARCHIVED))
return
profile?.let { profile ->
if (profile.archived) {
d(TAG, "The profile $profileId is archived")
taskCallback.onError(ApiError(TAG, ERROR_PROFILE_ARCHIVED))
return
}
else if (profile.shouldArchive()) {
d(TAG, "The profile $profileId's year ended on ${profile.dateYearEnd}, archiving")
ProfileArchiver(app, profile)
}
if (profile.isBeforeYear()) {
d(TAG, "The profile $profileId's school year has not started yet; aborting sync")
cancel()
taskCallback.onCompleted()
return
}
profile.registerName?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheckAt < currentTimeUnix()) {
val api = SzkolnyApi(app)
api.runCatching({
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
status = availability[registerName]
}, onError = {
taskCallback.onError(it.toApiError(TAG))
return
})
}
if (status?.available != true
|| status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) {
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
EventBus.getDefault().postSticky(
RegisterAvailabilityEvent(app.config.sync.registerAvailability)
)
}
cancel()
taskCallback.onCompleted()
return
}
}
}
edziennikInterface = when (loginStore.type) {
LOGIN_TYPE_LIBRUS -> Librus(app, profile, loginStore, taskCallback)
LOGIN_TYPE_MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback)
LOGIN_TYPE_VULCAN -> Vulcan(app, profile, loginStore, taskCallback)
LOGIN_TYPE_IDZIENNIK -> Idziennik(app, profile, loginStore, taskCallback)
LOGIN_TYPE_EDUDZIENNIK -> Edudziennik(app, profile, loginStore, taskCallback)
LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback)
else -> null
}
@ -100,6 +152,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
}
override fun cancel() {
d(TAG, "Task ${toString()} cancelling...")
edziennikInterface?.cancel()
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-8-25.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik
import android.content.Intent
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.Intent
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
class ProfileArchiver(val app: App, val profile: Profile) {
companion object {
private const val TAG = "ProfileArchiver"
}
init {
if (profile.archiveId == null)
profile.archiveId = profile.id
d(TAG, "Processing ${profile.name}#${profile.id}, archiveId = ${profile.archiveId}")
profile.archived = true
app.db.profileDao().add(profile)
//app.db.metadataDao().setAllSeen(profile.id, true)
app.db.notificationDao().clear(profile.id)
app.db.endpointTimerDao().clear(profile.id)
d(TAG, "Archived profile ${profile.id} saved")
profile.archived = false
// guess the nearest school year
val today = Date.getToday()
profile.studentSchoolYearStart = when {
today.month <= profile.dateYearEnd.month -> today.year - 1
else -> today.year
}
// set default semester dates
profile.dateSemester1Start = Date(profile.studentSchoolYearStart, 9, 1)
profile.dateSemester2Start = Date(profile.studentSchoolYearStart + 1, 2, 1)
profile.dateYearEnd = Date(profile.studentSchoolYearStart + 1, 6, 30)
val oldId = profile.id
val newId = (app.db.profileDao().lastId ?: profile.id) + 1
profile.id = newId
profile.subname = "Nowy rok szkolny - ${profile.studentSchoolYearStart}"
profile.studentClassName = null
d(TAG, "New profile ID for ${profile.name}: ${profile.id}")
when (profile.loginStoreType) {
LOGIN_TYPE_LIBRUS -> {
profile.removeStudentData("isPremium")
profile.removeStudentData("pushDeviceId")
profile.removeStudentData("startPointsSemester1")
profile.removeStudentData("startPointsSemester2")
profile.removeStudentData("enablePointGrades")
profile.removeStudentData("enableDescriptiveGrades")
}
LOGIN_TYPE_MOBIDZIENNIK -> {
}
LOGIN_TYPE_VULCAN -> {
// DataVulcan.isApiLoginValid() returns false so it will update the semester
profile.removeStudentData("currentSemesterEndDate")
profile.removeStudentData("studentSemesterId")
profile.removeStudentData("studentSemesterNumber")
profile.removeStudentData("semester1Id")
profile.removeStudentData("semester2Id")
profile.removeStudentData("studentClassId")
}
LOGIN_TYPE_IDZIENNIK -> {
profile.removeStudentData("schoolYearId")
}
LOGIN_TYPE_EDUDZIENNIK -> {
}
LOGIN_TYPE_PODLASIE -> {
}
}
d(TAG, "Processed student data: ${profile.studentData}")
app.db.profileDao().add(profile)
if (app.profileId == oldId) {
val intent = Intent(
Intent.ACTION_MAIN,
"profileId" to newId
)
app.sendBroadcast(intent)
}
}
}

View File

@ -110,7 +110,6 @@ class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStor
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
callback.onCompleted()
}
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {

View File

@ -59,7 +59,7 @@ class EdudziennikFirstLogin(val data: DataEdudziennik, val onSuccess: () -> Unit
profileList.add(profile)
}
EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore))
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-14
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.helper
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.FileCallbackHandler
import pl.szczodrzynski.edziennik.data.api.ERROR_FILE_DOWNLOAD
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.SYSTEM_USER_AGENT
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.utils.Utils
import java.io.File
class DownloadAttachment(
fileUrl: String,
val onSuccess: (file: File) -> Unit,
val onProgress: (written: Long, total: Long) -> Unit,
val onError: (apiError: ApiError) -> Unit
) {
companion object {
private const val TAG = "DownloadAttachment"
}
init {
val targetFile = Utils.getStorageDir()
val callback = object : FileCallbackHandler(targetFile) {
override fun onSuccess(file: File?, response: Response?) {
if (file == null) {
onError(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withResponse(response))
return
}
try {
onSuccess(file)
} catch (e: Exception) {
onError(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withResponse(response)
.withThrowable(e))
}
}
override fun onProgress(bytesWritten: Long, bytesTotal: Long) {
try {
this@DownloadAttachment.onProgress(bytesWritten, bytesTotal)
} catch (e: Exception) {
onError(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withThrowable(e))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
onError(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url(fileUrl)
.userAgent(SYSTEM_USER_AGENT)
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -133,7 +133,6 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
callback.onCompleted()
}
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {

View File

@ -89,7 +89,7 @@ class IdziennikFirstLogin(val data: DataIdziennik, val onSuccess: () -> Unit) {
profileList.add(profile)
}
EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore))
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}

View File

@ -70,6 +70,14 @@ class IdziennikLoginWeb(val data: DataIdziennik, val onSuccess: () -> Unit) {
data.webSelectedRegister = registerId
}
// for profiles created after archiving
data.schoolYearId = Regexes.IDZIENNIK_LOGIN_FIRST_SCHOOL_YEAR.find(text)?.let {
it[1].toIntOrNull()
} ?: data.schoolYearId
data.profile?.studentClassName = Regexes.IDZIENNIK_LOGIN_FIRST_STUDENT.findAll(text)
.firstOrNull { it[1].toIntOrNull() == data.registerId }
?.let { "${it[5]} ${it[6]}" } ?: data.profile?.studentClassName
data.profile?.let { profile ->
Regexes.IDZIENNIK_WEB_LUCKY_NUMBER.find(text)?.also {
val number = it[1].toIntOrNull() ?: return@also

View File

@ -120,7 +120,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiLogin: String? = null
var apiLogin: String?
get() { mApiLogin = mApiLogin ?: profile?.getStudentData("accountLogin", null); return mApiLogin }
set(value) { profile?.putStudentData("accountLogin", value) ?: return; mApiLogin = value }
set(value) { profile?.putStudentData("accountLogin", value); mApiLogin = value }
/**
* A Synergia password.
* Used: for login (API Login Method) in Synergia mode.
@ -129,7 +129,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiPassword: String? = null
var apiPassword: String?
get() { mApiPassword = mApiPassword ?: profile?.getStudentData("accountPassword", null); return mApiPassword }
set(value) { profile?.putStudentData("accountPassword", value) ?: return; mApiPassword = value }
set(value) { profile?.putStudentData("accountPassword", value); mApiPassword = value }
/**
* A JST login Code.
@ -138,8 +138,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiCode: String? = null
var apiCode: String?
get() { mApiCode = mApiCode ?: loginStore.getLoginData("accountCode", null); return mApiCode }
set(value) {
loginStore.putLoginData("accountCode", value); mApiCode = value }
set(value) { profile?.putStudentData("accountCode", value); mApiCode = value }
/**
* A JST login PIN.
* Used only during first login in JST mode.
@ -147,8 +146,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiPin: String? = null
var apiPin: String?
get() { mApiPin = mApiPin ?: loginStore.getLoginData("accountPin", null); return mApiPin }
set(value) {
loginStore.putLoginData("accountPin", value); mApiPin = value }
set(value) { profile?.putStudentData("accountPin", value); mApiPin = value }
/**
* A Synergia API access token.
@ -277,4 +275,10 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
var timetableNotPublic: Boolean
get() { mTimetableNotPublic = mTimetableNotPublic ?: profile?.getStudentData("timetableNotPublic", false); return mTimetableNotPublic ?: false }
set(value) { profile?.putStudentData("timetableNotPublic", value) ?: return; mTimetableNotPublic = value }
/**
* Set to false when Recaptcha helper doesn't provide a working token.
* When it's set to false uses Synergia for messages.
*/
var messagesLoginSuccessful: Boolean = true
}

View File

@ -13,9 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.Librus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetRecipientList
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaGetHomework
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaHomeworkGetAttachment
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaMarkAllAnnouncementsAsRead
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
@ -91,9 +89,8 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
override fun getMessage(message: MessageFull) {
login(LOGIN_METHOD_LIBRUS_MESSAGES) {
LibrusMessagesGetMessage(data, message) {
completed()
}
if (data.messagesLoginSuccessful) LibrusMessagesGetMessage(data, message) { completed() }
else LibrusSynergiaGetMessage(data, message) { completed() }
}
}
@ -124,10 +121,9 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {
when (owner) {
is Message -> {
login(LOGIN_METHOD_LIBRUS_MESSAGES) {
LibrusMessagesGetAttachment(data, owner, attachmentId, attachmentName) {
completed()
}
login(LOGIN_METHOD_LIBRUS_SYNERGIA) {
if (data.messagesLoginSuccessful) LibrusMessagesGetAttachment(data, owner, attachmentId, attachmentName) { completed() }
LibrusSynergiaGetAttachment(data, owner, attachmentId, attachmentName) { completed() }
}
}
is EventFull -> {
@ -161,7 +157,6 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
callback.onCompleted()
}
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {

View File

@ -50,6 +50,8 @@ const val ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS = 1130
const val ENDPOINT_LIBRUS_SYNERGIA_INFO = 2010
const val ENDPOINT_LIBRUS_SYNERGIA_GRADES = 2020
const val ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK = 2030
const val ENDPOINT_LIBRUS_SYNERGIA_MESSAGES_RECEIVED = 2040
const val ENDPOINT_LIBRUS_SYNERGIA_MESSAGES_SENT = 2050
const val ENDPOINT_LIBRUS_MESSAGES_RECEIVED = 3010
const val ENDPOINT_LIBRUS_MESSAGES_SENT = 3020
const val ENDPOINT_LIBRUS_MESSAGES_TRASH = 3030

View File

@ -36,11 +36,14 @@ class LibrusRecaptchaHelper(
}
private var timeout: Job? = null
private var timedOut = false
inner class WebViewClient : android.webkit.WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
timeout?.cancel()
onSuccess(url)
if (!timedOut) {
onSuccess(url)
}
return true
}
}
@ -50,6 +53,7 @@ class LibrusRecaptchaHelper(
webView.loadDataWithBaseURL(url, html, "text/html", "UTF-8", null)
}
timeout = startCoroutineTimer(delayMillis = 10000L) {
timedOut = true
onTimeout()
}
}

View File

@ -8,6 +8,7 @@ import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusMessagesGetList
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaGetMessages
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaHomework
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.LibrusSynergiaInfo
import pl.szczodrzynski.edziennik.data.db.entity.Message
@ -201,17 +202,27 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_student_info)
LibrusSynergiaInfo(data, lastSync, onSuccess)
}
ENDPOINT_LIBRUS_SYNERGIA_MESSAGES_RECEIVED -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
LibrusSynergiaGetMessages(data, type = Message.TYPE_RECEIVED, lastSync = lastSync, onSuccess = onSuccess)
}
ENDPOINT_LIBRUS_SYNERGIA_MESSAGES_SENT -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox)
LibrusSynergiaGetMessages(data, type = Message.TYPE_SENT, lastSync = lastSync, onSuccess = onSuccess)
}
/**
* MESSAGES
*/
ENDPOINT_LIBRUS_MESSAGES_RECEIVED -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
LibrusMessagesGetList(data, type = Message.TYPE_RECEIVED, lastSync = lastSync, onSuccess = onSuccess)
if (data.messagesLoginSuccessful) LibrusMessagesGetList(data, type = Message.TYPE_RECEIVED, lastSync = lastSync, onSuccess = onSuccess)
else LibrusSynergiaGetMessages(data, type = Message.TYPE_RECEIVED, lastSync = lastSync, onSuccess = onSuccess)
}
ENDPOINT_LIBRUS_MESSAGES_SENT -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox)
LibrusMessagesGetList(data, type = Message.TYPE_SENT, lastSync = lastSync, onSuccess = onSuccess)
if (data.messagesLoginSuccessful) LibrusMessagesGetList(data, type = Message.TYPE_SENT, lastSync = lastSync, onSuccess = onSuccess)
else LibrusSynergiaGetMessages(data, type = Message.TYPE_SENT, lastSync = lastSync, onSuccess = onSuccess)
}
else -> onSuccess(endpointId)

View File

@ -91,6 +91,8 @@ open class LibrusSynergia(open val data: DataLibrus, open val lastSync: Long?) {
}
fun redirectUrlGet(tag: String, url: String, onSuccess: (url: String) -> Unit) {
d(tag, "Request: Librus/Synergia - $url")
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response) {
val redirectUrl = response.headers().get("Location")

View File

@ -0,0 +1,24 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia
import pl.szczodrzynski.edziennik.data.api.LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.LibrusSandboxDownloadAttachment
import pl.szczodrzynski.edziennik.data.db.entity.Message
class LibrusSynergiaGetAttachment(override val data: DataLibrus,
val message: Message,
val attachmentId: Long,
val attachmentName: String,
val onSuccess: () -> Unit
) : LibrusSynergia(data, null) {
companion object {
const val TAG = "LibrusSynergiaGetAttachment"
}
init {
redirectUrlGet(TAG, "$LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL/${message.id}/$attachmentId") { url ->
LibrusSandboxDownloadAttachment(data, url, message, attachmentId, attachmentName, onSuccess)
}
}
}

View File

@ -0,0 +1,160 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia
import org.greenrobot.eventbus.EventBus
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.swapFirstLastName
import pl.szczodrzynski.edziennik.utils.models.Date
class LibrusSynergiaGetMessage(override val data: DataLibrus,
private val messageObject: MessageFull,
val onSuccess: () -> Unit) : LibrusSynergia(data, null) {
companion object {
const val TAG = "LibrusSynergiaGetMessage"
}
init {
val endpoint = when (messageObject.type) {
Message.TYPE_SENT -> "wiadomosci/1/6/${messageObject.id}/f0"
else -> "wiadomosci/1/5/${messageObject.id}/f0"
}
data.profile?.also { profile ->
synergiaGet(TAG, endpoint) { text ->
val doc = Jsoup.parse(text)
val messageElement = doc.select(".container-message tr")[0].child(1)
val detailsElement = messageElement.child(1)
val readElement = messageElement.children().last()
val body = messageElement.select(".container-message-content").html()
messageObject.apply {
this.body = body
clearAttachments()
if (messageElement.children().size >= 5) {
messageElement.child(3).select("tr").forEachIndexed { i, attachment ->
if (i == 0) return@forEachIndexed // Skip the header
val filename = attachment.child(0).text().trim()
val attachmentId = "wiadomosci\\\\/pobierz_zalacznik\\\\/[0-9]+?\\\\/([0-9]+)\"".toRegex()
.find(attachment.select("img").attr("onclick"))?.get(1)
?: return@forEachIndexed
addAttachment(attachmentId.toLong(), filename, -1)
}
}
}
val messageRecipientList = mutableListOf<MessageRecipientFull>()
when (messageObject.type) {
Message.TYPE_RECEIVED -> {
val senderFullName = detailsElement.child(0).select(".left").text()
val senderGroupName = "\\[(.+?)]".toRegex().find(senderFullName)?.get(1)?.trim()
data.teacherList.singleOrNull { it.id == messageObject.senderId }?.apply {
setTeacherType(when (senderGroupName) {
/* https://api.librus.pl/2.0/Messages/Role */
"Pomoc techniczna Librus", "SuperAdministrator" -> Teacher.TYPE_SUPER_ADMIN
"Administrator szkoły" -> Teacher.TYPE_SCHOOL_ADMIN
"Dyrektor Szkoły" -> Teacher.TYPE_PRINCIPAL
"Nauczyciel" -> Teacher.TYPE_TEACHER
"Rodzic", "Opiekun" -> Teacher.TYPE_PARENT
"Sekretariat" -> Teacher.TYPE_SECRETARIAT
"Uczeń" -> Teacher.TYPE_STUDENT
"Pedagog/Psycholog szkolny" -> Teacher.TYPE_PEDAGOGUE
"Pracownik biblioteki" -> Teacher.TYPE_LIBRARIAN
"Inny specjalista" -> Teacher.TYPE_SPECIALIST
"Jednostka Nadrzędna" -> {
typeDescription = "Jednostka Nadrzędna"
Teacher.TYPE_OTHER
}
"Jednostka Samorządu Terytorialnego" -> {
typeDescription = "Jednostka Samorządu Terytorialnego"
Teacher.TYPE_OTHER
}
else -> Teacher.TYPE_OTHER
})
}
val readDateText = readElement.select(".left").text()
val readDate = when (readDateText.isNotNullNorEmpty()) {
true -> Date.fromIso(readDateText)
else -> 0
}
val messageRecipientObject = MessageRecipientFull(
profileId = profileId,
id = -1,
messageId = messageObject.id,
readDate = readDate
)
messageRecipientObject.fullName = profile.accountName
?: profile.studentNameLong
messageRecipientList.add(messageRecipientObject)
}
Message.TYPE_SENT -> {
readElement.select("tr").forEachIndexed { i, receiver ->
if (i == 0) return@forEachIndexed // Skip the header
val receiverFullName = receiver.child(0).text()
val receiverName = receiverFullName.split('(')[0].swapFirstLastName()
val teacher = data.teacherList.singleOrNull { it.fullName == receiverName }
val receiverId = teacher?.id ?: -1
val readDate = when (val readDateText = receiver.child(1).text().trim()) {
"NIE" -> 0
else -> Date.fromIso(readDateText)
}
val messageRecipientObject = MessageRecipientFull(
profileId = profileId,
id = receiverId,
messageId = messageObject.id,
readDate = readDate
)
messageRecipientObject.fullName = receiverName
messageRecipientList.add(messageRecipientObject)
}
}
}
if (!messageObject.seen) {
data.setSeenMetadataList.add(Metadata(
messageObject.profileId,
Metadata.TYPE_MESSAGE,
messageObject.id,
true,
true
))
}
messageObject.recipients = messageRecipientList
data.messageRecipientList.addAll(messageRecipientList)
data.messageList.add(messageObject)
data.messageListReplace = true
EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess()
}
} ?: onSuccess()
}
}

View File

@ -0,0 +1,116 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_NOT_IMPLEMENTED
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
class LibrusSynergiaGetMessages(override val data: DataLibrus,
override val lastSync: Long?,
private val type: Int = Message.TYPE_RECEIVED,
archived: Boolean = false,
val onSuccess: (Int) -> Unit) : LibrusSynergia(data, lastSync) {
companion object {
const val TAG = "LibrusSynergiaGetMessages"
}
init {
val endpoint = when (type) {
Message.TYPE_RECEIVED -> "wiadomosci/5"
Message.TYPE_SENT -> "wiadomosci/6"
else -> null
}
val endpointId = when (type) {
Message.TYPE_RECEIVED -> ENDPOINT_LIBRUS_SYNERGIA_MESSAGES_RECEIVED
else -> ENDPOINT_LIBRUS_SYNERGIA_MESSAGES_SENT
}
if (endpoint != null) {
synergiaGet(TAG, endpoint) { text ->
val doc = Jsoup.parse(text)
fun getRecipientId(name: String): Long = data.teacherList.singleOrNull {
it.fullNameLastFirst == name
}?.id ?: {
val teacherObject = Teacher(
profileId,
-1 * Utils.crc16(name.swapFirstLastName().toByteArray()).toLong(),
name.splitName()?.second!!,
name.splitName()?.first!!
)
data.teacherList.put(teacherObject.id, teacherObject)
teacherObject.id
}.invoke()
doc.select(".decorated.stretch tbody > tr").forEach { messageElement ->
val url = messageElement.select("a").first().attr("href")
val id = Regexes.LIBRUS_MESSAGE_ID.find(url)?.get(1)?.toLong() ?: return@forEach
val subject = messageElement.child(3).text()
val sentDate = Date.fromIso(messageElement.child(4).text())
val recipientName = messageElement.child(2).text().split('(')[0].fixName()
val recipientId = getRecipientId(recipientName)
val read = messageElement.child(2).attr("style").isNullOrBlank()
val senderId = when (type) {
Message.TYPE_RECEIVED -> recipientId
else -> null
}
val receiverId = when (type) {
Message.TYPE_RECEIVED -> -1
else -> recipientId
}
val notified = when (type) {
Message.TYPE_SENT -> true
else -> read || profile?.empty ?: false
}
val messageObject = Message(
profileId = profileId,
id = id,
type = type,
subject = subject,
body = null,
senderId = senderId,
addedDate = sentDate
)
val messageRecipientObject = MessageRecipient(
profileId,
receiverId,
-1,
if (read) 1 else 0,
id
)
messageObject.hasAttachments = !messageElement.child(1).select("img").isEmpty()
data.messageList.add(messageObject)
data.messageRecipientList.add(messageRecipientObject)
data.setSeenMetadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,
id,
notified,
notified
))
}
when (type) {
Message.TYPE_RECEIVED -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_RECEIVED, SYNC_ALWAYS)
Message.TYPE_SENT -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_SENT, DAY, MainActivity.DRAWER_ITEM_MESSAGES)
}
onSuccess(endpointId)
}
} else {
data.error(TAG, ERROR_NOT_IMPLEMENTED)
onSuccess(endpointId)
}
}
}

View File

@ -33,7 +33,7 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
val accounts = json.getJsonArray("accounts")
if (accounts == null || accounts.size() < 1) {
EventBus.getDefault().post(FirstLoginFinishedEvent(listOf(), data.loginStore))
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore))
onSuccess()
return@portalGet
}
@ -81,7 +81,7 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
profileList.add(profile)
}
EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore))
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}
@ -116,14 +116,15 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
).apply {
studentData["isPremium"] = account?.getBoolean("IsPremium") == true || account?.getBoolean("IsPremiumDemo") == true
studentData["accountId"] = account.getInt("Id") ?: 0
studentData["accountLogin"] = login
studentData["accountLogin"] = data.apiLogin ?: login
studentData["accountPassword"] = data.apiPassword
studentData["accountToken"] = data.apiAccessToken
studentData["accountTokenTime"] = data.apiTokenExpiryTime
studentData["accountRefreshToken"] = data.apiRefreshToken
}
profileList.add(profile)
EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore))
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}

View File

@ -18,6 +18,7 @@ import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection.*
@Suppress("ConvertSecondaryConstructorToPrimary")
class LibrusLoginApi {
companion object {
private const val TAG = "LoginLibrusApi"

View File

@ -38,14 +38,18 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
text?.contains("grecaptcha.ready") == true -> {
val url = response?.request()?.url()?.toString() ?: run {
data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text)
//data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text)
data.messagesLoginSuccessful = false
onSuccess()
return
}
LibrusRecaptchaHelper(data.app, url, text, onSuccess = { newUrl ->
loginWithSynergia(newUrl)
}, onTimeout = {
data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_TIMEOUT, response, text)
//data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_TIMEOUT, response, text)
data.messagesLoginSuccessful = false
onSuccess()
})
}
@ -55,7 +59,11 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
}
text?.contains("<message>Niepoprawny login i/lub hasło.</message>") == true -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text)
text?.contains("stop.png") == true -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
text?.contains("eAccessDeny") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text?.contains("eAccessDeny") == true -> {
// data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
data.messagesLoginSuccessful = false
onSuccess()
}
text?.contains("OffLine") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text)
text?.contains("<status>error</status>") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text)
text?.contains("<type>eVarWhitThisNameNotExists</type>") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)

View File

@ -66,7 +66,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
override fun onSuccess(text: String, response: Response) {
val location = response.headers().get("Location")
if (location != null) {
val authMatcher = Pattern.compile("http://localhost/bar\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
when {
authMatcher.find() -> {
accessToken(authMatcher.group(1), null)
@ -127,7 +127,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
.callback(object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response) {
val location = response.headers()?.get("Location")
if (location == "http://localhost/bar?command=close") {
if (location == "$LIBRUS_REDIRECT_URL?command=close") {
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
@ -146,12 +146,14 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
}
val error = if (response.code() == 200) null else
json.getJsonArray("errors")?.getString(0)
?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString
error?.let { code ->
when {
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
// this doesn't work anyway: `errors` is an object with `g-recaptcha-response` set
code.contains("robotem") -> ERROR_CAPTCHA_LIBRUS_PORTAL
code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
}.let { errorCode ->
data.error(ApiError(TAG, errorCode)

View File

@ -130,7 +130,6 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
callback.onCompleted()
}
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {

View File

@ -44,7 +44,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
dataDays.remove(date.value)
val subjectId = data.subjectList.singleOrNull { it.longName == lesson[5] }?.id ?: -1
val subjectId = data.subjectList.singleOrNull { it.longName == lesson[5].trim() }?.id ?: -1
val teacherId = data.teacherList.singleOrNull { it.fullNameLastFirst == (lesson[7]+" "+lesson[6]).fixName() }?.id ?: -1
val teamId = data.teamList.singleOrNull { it.name == lesson[8]+lesson[9] }?.id ?: -1
val classroom = lesson[11]

View File

@ -91,8 +91,11 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
Regexes.MOBIDZIENNIK_ATTENDANCE_TABLE.findAll(text).forEach { tableResult ->
val table = tableResult[1]
val lessonDates = mutableListOf<Date>()
val entries = mutableListOf<String>()
val ranges = mutableListOf<MatchResult?>()
Regexes.MOBIDZIENNIK_ATTENDANCE_LESSON_COUNT.findAll(table).forEach {
val date = Date.fromY_m_d(it[1])
for (i in 0 until (it[2].toIntOrNull() ?: 0)) {
@ -101,96 +104,52 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
}
Regexes.MOBIDZIENNIK_ATTENDANCE_ENTRIES.findAll(table).mapTo(entries) { it[1] }
Regexes.MOBIDZIENNIK_ATTENDANCE_COLUMNS.findAll(table).forEach { columns ->
var index = 0
Regexes.MOBIDZIENNIK_ATTENDANCE_COLUMN.findAll(columns[1]).forEach { column ->
if (column[1].contains("colspan")) {
val colspan =
Regexes.MOBIDZIENNIK_ATTENDANCE_COLUMN_SPAN.find(column[1])
?.get(1)
?.toIntOrNull() ?: 0
entries.addAll(index, List(colspan) { "" })
ranges.addAll(List(colspan) { null })
index += colspan
}
else {
val range = Regexes.MOBIDZIENNIK_ATTENDANCE_RANGE.find(column[2])
ranges.add(range)
index++
}
}
}
val dateIterator = lessonDates.iterator()
val entriesIterator = entries.iterator()
Regexes.MOBIDZIENNIK_ATTENDANCE_RANGE.findAll(table).let { ranges ->
val count = ranges.count()
// verify the lesson count is the same as dates & entries
if (count != lessonDates.count() || count != entries.count())
val count = ranges.count()
// verify the lesson count is the same as dates & entries
if (count != lessonDates.count() || count != entries.count())
return@forEach
ranges.forEach { range ->
val lessonDate = dateIterator.next()
val entry = entriesIterator.next()
if (range == null || entry.isBlank())
return@forEach
ranges.forEach { range ->
val lessonDate = dateIterator.next()
var entry = entriesIterator.next()
if (entry.isBlank())
return@forEach
val startTime = Time.fromH_m(range[1])
val startTime = Time.fromH_m(range[1])
range[2].split(" / ").mapNotNull { Regexes.MOBIDZIENNIK_ATTENDANCE_LESSON.find(it) }.forEachIndexed { index, lesson ->
val topic = lesson[1].substringAfter(" - ", missingDelimiterValue = "").takeIf { it.isNotBlank() }
if (topic?.startsWith("Lekcja odwołana: ") == true || entry.isEmpty())
return@forEachIndexed
val subjectName = lesson[1].substringBefore(" - ")
//val team = lesson[3]
val teacherName = lesson[3].fixName()
val teacherId = data.teacherList.singleOrNull { it.fullNameLastFirst == teacherName }?.id ?: -1
val subjectId = data.subjectList.singleOrNull { it.longName == subjectName }?.id ?: -1
var typeSymbol = ""
for (symbol in typeSymbols) {
if (entry.startsWith(symbol) && symbol.length > typeSymbol.length)
typeSymbol = symbol
}
entry = entry.removePrefix(typeSymbol)
var isCounted = true
val baseType = when (typeSymbol) {
"." -> TYPE_PRESENT
"|" -> TYPE_ABSENT
"+" -> TYPE_ABSENT_EXCUSED
"s" -> TYPE_BELATED
"z" -> TYPE_RELEASED
else -> {
isCounted = false
when (typeSymbol) {
"e" -> TYPE_PRESENT_CUSTOM
"en" -> TYPE_ABSENT
"ep" -> TYPE_PRESENT_CUSTOM
else -> TYPE_UNKNOWN
}
}
}
val typeName = types?.get(typeSymbol) ?: ""
val typeShort = when (baseType) {
TYPE_UNKNOWN -> typeSymbol
else -> data.app.attendanceManager.getTypeShort(baseType)
}
val semester = data.profile?.dateToSemester(lessonDate) ?: 1
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson[0].hashCode() and 0xFFFF) + index
val attendanceObject = Attendance(
profileId = profileId,
id = id,
baseType = baseType,
typeName = typeName,
typeShort = typeShort,
typeSymbol = typeSymbol,
typeColor = null,
date = lessonDate,
startTime = startTime,
semester = semester,
teacherId = teacherId,
subjectId = subjectId
).also {
it.lessonTopic = topic
it.isCounted = isCounted
}
data.attendanceList.add(attendanceObject)
if (baseType != TYPE_PRESENT) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_ATTENDANCE,
id,
data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN,
data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN
))
}
}
range[2].split(" / ").mapNotNull {
Regexes.MOBIDZIENNIK_ATTENDANCE_LESSON.find(it)
}.forEachIndexed { index, lesson ->
processEntry(
index,
lesson,
lessonDate,
startTime,
entry,
types,
typeSymbols
)
}
}
}
@ -200,4 +159,97 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
onSuccess()
}
}
private fun processEntry(
index: Int,
lesson: MatchResult,
lessonDate: Date,
startTime: Time,
entry: String,
types: Map<String?, String?>?,
typeSymbols: List<String>
) {
var entry = entry
val topic = lesson[1].substringAfter(" - ", missingDelimiterValue = "").takeIf { it.isNotBlank() }
if (topic?.startsWith("Lekcja odwołana: ") == true || entry.isEmpty())
return
val subjectName = lesson[1].substringBefore(" - ")
//val team = lesson[3]
val teacherName = lesson[3].fixName()
val teacherId = data.teacherList.singleOrNull { it.fullNameLastFirst == teacherName }?.id ?: -1
val subjectId = data.subjectList.singleOrNull { it.longName == subjectName }?.id ?: -1
var typeSymbol = ""
for (symbol in typeSymbols) {
if (entry.startsWith(symbol) && symbol.length > typeSymbol.length)
typeSymbol = symbol
}
entry = entry.removePrefix(typeSymbol)
var isCounted = true
val baseType = when (typeSymbol) {
"." -> TYPE_PRESENT
"|" -> TYPE_ABSENT
"+" -> TYPE_ABSENT_EXCUSED
"s" -> TYPE_BELATED
"z" -> TYPE_RELEASED
else -> {
isCounted = false
when (typeSymbol) {
"e" -> TYPE_PRESENT_CUSTOM
"en" -> TYPE_ABSENT
"ep" -> TYPE_PRESENT_CUSTOM
else -> TYPE_UNKNOWN
}
}
}
val typeName = types?.get(typeSymbol) ?: ""
val typeColor = when (typeSymbol) {
"e" -> 0xff673ab7
"en" -> 0xffec407a
"ep" -> 0xff4caf50
else -> null
}?.toInt()
val typeShort = if (isCounted)
data.app.attendanceManager.getTypeShort(baseType)
else
typeSymbol
val semester = data.profile?.dateToSemester(lessonDate) ?: 1
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson[0].hashCode() and 0xFFFF) + index
val attendanceObject = Attendance(
profileId = profileId,
id = id,
baseType = baseType,
typeName = typeName,
typeShort = typeShort,
typeSymbol = typeSymbol,
typeColor = typeColor,
date = lessonDate,
startTime = startTime,
semester = semester,
teacherId = teacherId,
subjectId = subjectId
).also {
it.lessonTopic = topic
it.isCounted = isCounted
}
data.attendanceList.add(attendanceObject)
if (baseType != TYPE_PRESENT) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_ATTENDANCE,
id,
data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN,
data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN
))
}
}
}

View File

@ -56,20 +56,21 @@ class MobidziennikWebGetRecipientList(override val data: DataMobidziennik,
}
private fun processRecipient(listType: Int, listName: String, recipient: JsonObject) {
val id = recipient.getLong("id") ?: -1
val id = recipient.getString("id") ?: return
val idLong = id.replace(Regexes.NOT_DIGITS, "").toLongOrNull() ?: return
// get teacher by ID or create it
val teacher = data.teacherList[id] ?: Teacher(data.profileId, id).apply {
val teacher = data.teacherList[idLong] ?: Teacher(data.profileId, idLong).apply {
val fullName = recipient.getString("nazwa")?.fixName()
name = fullName ?: ""
fullName?.splitName()?.let {
name = it.second
surname = it.first
}
data.teacherList[id] = this
data.teacherList[idLong] = this
}
teacher.apply {
loginId = id.toString()
loginId = id
when (listType) {
1 -> setTeacherType(Teacher.TYPE_PRINCIPAL)
2 -> setTeacherType(Teacher.TYPE_TEACHER)

View File

@ -85,7 +85,7 @@ class MobidziennikFirstLogin(val data: DataMobidziennik, val onSuccess: () -> Un
profileList.add(profile)
}
EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore))
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_PODLASIE_API
import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.*
import kotlin.text.replace
class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
fun isApiLoginValid() = apiToken.isNotNullNorEmpty()
override fun satisfyLoginMethods() {
loginMethods.clear()
if (isApiLoginValid())
loginMethods += LOGIN_METHOD_PODLASIE_API
}
override fun generateUserCode(): String = "$schoolShortName:$loginShort:${studentId?.crc32()}"
/* _
/\ (_)
/ \ _ __ _
/ /\ \ | '_ \| |
/ ____ \| |_) | |
/_/ \_\ .__/|_|
| |
|*/
private var mApiToken: String? = null
var apiToken: String?
get() { mApiToken = mApiToken ?: loginStore.getLoginData("apiToken", null); return mApiToken }
set(value) { loginStore.putLoginData("apiToken", value); mApiToken = value }
private var mApiUrl: String? = null
var apiUrl: String?
get() { mApiUrl = mApiUrl ?: profile?.getStudentData("apiUrl", null); return mApiUrl }
set(value) { profile?.putStudentData("apiUrl", value) ?: return; mApiUrl = value }
/* ____ _ _
/ __ \| | | |
| | | | |_| |__ ___ _ __
| | | | __| '_ \ / _ \ '__|
| |__| | |_| | | | __/ |
\____/ \__|_| |_|\___|*/
private var mStudentId: String? = null
var studentId: String?
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId }
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
private var mStudentLogin: String? = null
var studentLogin: String?
get() { mStudentLogin = mStudentLogin ?: profile?.getStudentData("studentLogin", null); return mStudentLogin }
set(value) { profile?.putStudentData("studentLogin", value) ?: return; mStudentLogin = value }
private var mSchoolName: String? = null
var schoolName: String?
get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName }
set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value }
private var mClassName: String? = null
var className: String?
get() { mClassName = mClassName ?: profile?.getStudentData("className", null); return mClassName }
set(value) { profile?.putStudentData("className", value) ?: return; mClassName = value }
private var mSchoolYear: String? = null
var schoolYear: String?
get() { mSchoolYear = mSchoolYear ?: profile?.getStudentData("schoolYear", null); return mSchoolYear }
set(value) { profile?.putStudentData("schoolYear", value) ?: return; mSchoolYear = value }
private var mCurrentSemester: Int? = null
var currentSemester: Int
get() { mCurrentSemester = mCurrentSemester ?: profile?.getStudentData("currentSemester", 0); return mCurrentSemester ?: 0 }
set(value) { profile?.putStudentData("currentSemester", value) ?: return; mCurrentSemester = value }
val schoolShortName: String?
get() = studentLogin?.split('@')?.get(1)?.replace(".podlaskie.pl", "")
val loginShort: String?
get() = studentLogin?.split('@')?.get(0)
fun getSubject(name: String): Subject {
val id = name.crc32()
return subjectList.singleOrNull { it.id == id } ?: run {
val subject = Subject(profileId, id, name, name)
subjectList.put(id, subject)
subject
}
}
fun getTeacher(firstName: String, lastName: String): Teacher {
val name = "$firstName $lastName".fixName()
return teacherList.singleOrNull { it.fullName == name } ?: run {
val id = name.crc32()
val teacher = Teacher(profileId, id, firstName, lastName)
teacherList.put(id, teacher)
teacher
}
}
fun getTeam(name: String? = null): Team {
if (name == "cała klasa" || name == null) return teamClass ?: run {
val id = className!!.crc32()
val teamCode = "$schoolShortName:$className"
val team = Team(profileId, id, className, Team.TYPE_CLASS, teamCode, -1)
teamList.put(id, team)
return team
} else {
val id = name.crc32()
val teamCode = "$schoolShortName:$name"
val team = Team(profileId, id, name, Team.TYPE_VIRTUAL, teamCode, -1)
teamList.put(id, team)
return team
}
}
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.edziennik.helper.DownloadAttachment
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieData
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin.PodlasieFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.podlasieLoginMethods
import pl.szczodrzynski.edziennik.data.api.prepare
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils
import java.io.File
class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
companion object {
const val TAG = "Podlasie"
}
val internalErrorList = mutableListOf<Int>()
val data: DataPodlasie
init {
data = DataPodlasie(app, profile, loginStore).apply {
callback = wrapCallback(this@Podlasie.callback)
satisfyLoginMethods()
}
}
private fun completed() {
data.saveData()
callback.onCompleted()
}
/* _______ _ _ _ _ _
|__ __| | /\ | | (_) | | |
| | | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___
| | | '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \
| | | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | |
|_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_|
__/ |
|__*/
override fun sync(featureIds: List<Int>, viewId: Int?, onlyEndpoints: List<Int>?, arguments: JsonObject?) {
data.arguments = arguments
data.prepare(podlasieLoginMethods, PodlasieFeatures, featureIds, viewId, onlyEndpoints)
Utils.d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
Utils.d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
PodlasieLogin(data) {
PodlasieData(data) {
completed()
}
}
}
override fun getMessage(message: MessageFull) {
}
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {
}
override fun markAllAnnouncementsAsRead() {
}
override fun getAnnouncement(announcement: AnnouncementFull) {
}
override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {
val fileUrl = attachmentName.substringAfter(":")
DownloadAttachment(fileUrl,
onSuccess = { file ->
val event = AttachmentGetEvent(
data.profileId,
owner,
attachmentId,
AttachmentGetEvent.TYPE_FINISHED,
file.absolutePath
)
val attachmentDataFile = File(Utils.getStorageDir(), ".${data.profileId}_${event.ownerId}_${event.attachmentId}")
Utils.writeStringToFile(attachmentDataFile, event.fileName)
EventBus.getDefault().postSticky(event)
completed()
},
onProgress = { written, _ ->
val event = AttachmentGetEvent(
data.profileId,
owner,
attachmentId,
AttachmentGetEvent.TYPE_PROGRESS,
bytesWritten = written
)
EventBus.getDefault().postSticky(event)
},
onError = { apiError ->
data.error(apiError)
})
}
override fun getRecipientList() {
}
override fun getEvent(eventFull: EventFull) {
}
override fun firstLogin() {
PodlasieFirstLogin(data) {
completed()
}
}
override fun cancel() {
Utils.d(TAG, "Cancelled")
data.cancel()
}
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() {
callback.onCompleted()
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}
override fun onStartProgress(stringRes: Int) {
callback.onStartProgress(stringRes)
}
override fun onError(apiError: ApiError) {
// TODO Error handling
when (apiError.errorCode) {
in internalErrorList -> {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
}
else -> callback.onError(apiError)
}
}
}
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie
import pl.szczodrzynski.edziennik.data.api.FEATURE_ALWAYS_NEEDED
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_PODLASIE_API
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_PODLASIE
import pl.szczodrzynski.edziennik.data.api.models.Feature
const val ENDPOINT_PODLASIE_API_MAIN = 1001
val PodlasieFeatures = listOf(
Feature(LOGIN_TYPE_PODLASIE, FEATURE_ALWAYS_NEEDED, listOf(
ENDPOINT_PODLASIE_API_MAIN to LOGIN_METHOD_PODLASIE_API
), listOf(LOGIN_METHOD_PODLASIE_API))
)

View File

@ -0,0 +1,108 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.RequestParams
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.JsonCallbackHandler
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getJsonObject
import pl.szczodrzynski.edziennik.toHexString
import pl.szczodrzynski.edziennik.utils.Utils
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
open class PodlasieApi(open val data: DataPodlasie, open val lastSync: Long?) {
companion object {
const val TAG = "PodlasieApi"
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
fun apiGet(tag: String, endpoint: String, onSuccess: (json: JsonObject) -> Unit) {
val url = PODLASIE_API_URL + endpoint
Utils.d(tag, "Request: Podlasie/Api - $url")
if (data.apiToken == null) {
data.error(tag, ERROR_PODLASIE_API_NO_TOKEN)
return
}
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null || response == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
val error = json.getJsonObject("system_message")?.getInt("code")
error?.let { code ->
when (code) {
0 -> ERROR_PODLASIE_API_DATA_MISSING
4 -> ERROR_LOGIN_PODLASIE_API_DEVICE_LIMIT
5 -> ERROR_LOGIN_PODLASIE_API_INVALID_TOKEN
200 -> null // Not an error
else -> ERROR_PODLASIE_API_OTHER
}?.let { errorCode ->
data.error(ApiError(tag, errorCode)
.withApiResponse(json)
.withResponse(response))
return@onSuccess
}
}
try {
onSuccess(json)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_PODLASIE_API_REQUEST)
.withResponse(response)
.withThrowable(e)
.withApiResponse(json))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url(url)
.userAgent(SYSTEM_USER_AGENT)
.requestParams(RequestParams(mapOf(
"token" to data.apiToken,
"securityToken" to getSecurityToken(),
"mobileId" to data.app.deviceId,
"ver" to PODLASIE_API_VERSION
)))
.callback(callback)
.build()
.enqueue()
}
private fun getSecurityToken(): String {
val format = SimpleDateFormat("yyyy-MM-dd HH", Locale.ENGLISH)
.also { it.timeZone = TimeZone.getTimeZone("Europe/Warsaw") }.format(System.currentTimeMillis())
val instance = MessageDigest.getInstance("SHA-256")
val digest = instance.digest("-EYlwYu8u16miVd8tT?oO7cvoUVQrQN0vr!$format".toByteArray()).toHexString()
val digest2 = instance.digest((data.apiToken ?: "").toByteArray()).toHexString()
return instance.digest("$digest$digest2".toByteArray()).toHexString()
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.ENDPOINT_PODLASIE_API_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api.PodlasieApiMain
import pl.szczodrzynski.edziennik.utils.Utils
class PodlasieData(val data: DataPodlasie, val onSuccess: () -> Unit) {
companion object {
const val TAG = "PodlasieData"
}
init {
nextEndpoint(onSuccess)
}
private fun nextEndpoint(onSuccess: () -> Unit) {
if (data.targetEndpointIds.isEmpty()) {
onSuccess()
return
}
if (data.cancelled) {
onSuccess()
return
}
val id = data.targetEndpointIds.firstKey()
val lastSync = data.targetEndpointIds.remove(id)
useEndpoint(id, lastSync) {
data.progress(data.progressStep)
nextEndpoint(onSuccess)
}
}
private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) {
Utils.d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync")
when (endpointId) {
ENDPOINT_PODLASIE_API_MAIN -> {
data.startProgress(R.string.edziennik_progress_endpoint_data)
PodlasieApiMain(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-13
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.util.*
class PodlasieApiEvents(val data: DataPodlasie, val rows: List<JsonObject>) {
init {
rows.forEach { event ->
val id = event.getLong("ExternalId") ?: return@forEach
val date = event.getString("DateFrom")?.let { Date.fromY_m_d(it) } ?: return@forEach
val time = event.getString("DateFrom")?.let { Time.fromY_m_d_H_m_s(it) }
?: return@forEach
val name = event.getString("Name")?.replace("&#34;", "\"") ?: ""
val description = event.getString("Description")?.replace("&#34;", "\"") ?: ""
val type = when (event.getString("Category")?.toLowerCase(Locale.getDefault())) {
"klasówka" -> Event.TYPE_EXAM
"praca domowa" -> Event.TYPE_HOMEWORK
"wycieczka" -> Event.TYPE_EXCURSION
else -> Event.TYPE_DEFAULT
}
val teacherFirstName = event.getString("PersonEnteringDataFirstName") ?: return@forEach
val teacherLastName = event.getString("PersonEnteringDataLastName") ?: return@forEach
val teacher = data.getTeacher(teacherFirstName, teacherLastName)
val lessonList = data.db.timetableDao().getAllForDateNow(data.profileId, date)
val lesson = lessonList.firstOrNull { it.startTime == time }
val addedDate = event.getString("CreateDate")?.let { Date.fromIso(it) }
?: System.currentTimeMillis()
val eventObject = Event(
profileId = data.profileId,
id = id,
date = date,
time = time,
topic = name,
color = null,
type = type,
teacherId = teacher.id,
subjectId = lesson?.subjectId ?: -1,
teamId = data.teamClass?.id ?: -1,
addedDate = addedDate
).apply {
homeworkBody = description
}
data.eventList.add(eventObject)
data.metadataList.add(
Metadata(
data.profileId,
if (type == Event.TYPE_HOMEWORK) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT,
id,
data.profile?.empty ?: false,
data.profile?.empty ?: false
))
}
data.toRemove.add(DataRemoveModel.Events.future())
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-13
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER1_FINAL
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER1_PROPOSED
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_FINAL
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_PROPOSED
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_FINAL
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_PROPOSED
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List<JsonObject>) {
init { data.profile?.also { profile ->
rows.forEach { grade ->
val id = grade.getLong("ExternalId") ?: return@forEach
val mark = grade.getString("Mark") ?: return@forEach
val proposedMark = grade.getString("ProposedMark") ?: "0"
val name = data.app.gradesManager.getGradeNumberName(mark)
val value = data.app.gradesManager.getGradeValue(name)
val semester = grade.getString("TermShortcut")?.length ?: return@forEach
val typeName = grade.getString("Type") ?: return@forEach
val type = when (typeName) {
"S" -> if (semester == 1) TYPE_SEMESTER1_FINAL else TYPE_SEMESTER2_FINAL
"Y", "R" -> TYPE_YEAR_FINAL
else -> return@forEach
}
val subjectName = grade.getString("SchoolSubject") ?: return@forEach
val subject = data.getSubject(subjectName)
val addedDate = if (profile.empty) profile.getSemesterStart(semester).inMillis
else System.currentTimeMillis()
val gradeObject = Grade(
profileId = data.profileId,
id = id,
name = name,
type = type,
value = value,
weight = 0f,
color = -1,
category = null,
description = null,
comment = null,
semester = semester,
teacherId = -1,
subjectId = subject.id,
addedDate = addedDate
)
data.gradeList.add(gradeObject)
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_GRADE,
id,
profile.empty,
profile.empty
))
if (proposedMark != "0") {
val proposedName = data.app.gradesManager.getGradeNumberName(proposedMark)
val proposedValue = data.app.gradesManager.getGradeValue(proposedName)
val proposedType = when (typeName) {
"S" -> if (semester == 1) TYPE_SEMESTER1_PROPOSED else TYPE_SEMESTER2_PROPOSED
"Y", "R" -> TYPE_YEAR_PROPOSED
else -> return@forEach
}
val proposedGradeObject = Grade(
profileId = data.profileId,
id = id * (-1),
name = proposedName,
type = proposedType,
value = proposedValue,
weight = 0f,
color = -1,
category = null,
description = null,
comment = null,
semester = semester,
teacherId = -1,
subjectId = subject.id,
addedDate = addedDate
)
data.gradeList.add(proposedGradeObject)
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_GRADE,
proposedGradeObject.id,
profile.empty,
profile.empty
))
}
}
data.toRemove.addAll(listOf(
TYPE_SEMESTER1_FINAL,
TYPE_SEMESTER1_PROPOSED,
TYPE_SEMESTER2_FINAL,
TYPE_SEMESTER2_PROPOSED,
TYPE_YEAR_FINAL,
TYPE_YEAR_PROPOSED
).map {
DataRemoveModel.Grades.allWithType(it)
})
}}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-13
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api
import android.graphics.Color
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.getFloat
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
class PodlasieApiGrades(val data: DataPodlasie, val rows: List<JsonObject>) {
init {
rows.forEach { grade ->
val id = grade.getLong("ExternalId") ?: return@forEach
val name = grade.getString("Mark") ?: return@forEach
val value = data.app.gradesManager.getGradeValue(name)
val weight = grade.getFloat("Weight") ?: 0f
val includeToAverage = grade.getInt("IncludeToAverage") != 0
val color = grade.getString("Color")?.let { Color.parseColor(it) } ?: -1
val category = grade.getString("Category") ?: ""
val comment = grade.getString("Comment") ?: ""
val semester = grade.getString("TermShortcut")?.length ?: data.currentSemester
val teacherFirstName = grade.getString("TeacherFirstName") ?: return@forEach
val teacherLastName = grade.getString("TeacherLastName") ?: return@forEach
val teacher = data.getTeacher(teacherFirstName, teacherLastName)
val subjectName = grade.getString("SchoolSubject") ?: return@forEach
val subject = data.getSubject(subjectName)
val addedDate = grade.getString("ReceivedDate")?.let { Date.fromY_m_d(it).inMillis }
?: System.currentTimeMillis()
val gradeObject = Grade(
profileId = data.profileId,
id = id,
name = name,
type = Grade.TYPE_NORMAL,
value = value,
weight = if (includeToAverage) weight else 0f,
color = color,
category = category,
description = null,
comment = comment,
semester = semester,
teacherId = teacher.id,
subjectId = subject.id,
addedDate = addedDate
)
data.gradeList.add(gradeObject)
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_GRADE,
id,
data.profile?.empty ?: false,
data.profile?.empty ?: false
))
}
data.toRemove.add(DataRemoveModel.Grades.allWithType(Grade.TYPE_NORMAL))
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-14
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.crc32
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
class PodlasieApiHomework(val data: DataPodlasie, val rows: List<JsonObject>) {
init {
rows.reversed().forEach { homework ->
val id = homework.getString("ExternalId")?.crc32() ?: return@forEach
val topic = homework.getString("Title")?.replace("&#34;", "\"") ?: ""
val description = homework.getString("Message")?.replace("&#34;", "\"") ?: ""
val date = Date.getToday()
val addedDate = System.currentTimeMillis()
val eventObject = Event(
profileId = data.profileId,
id = id,
date = date,
time = null,
topic = topic,
color = null,
type = Event.TYPE_HOMEWORK,
teacherId = -1,
subjectId = -1,
teamId = data.teamClass?.id ?: -1,
addedDate = addedDate
).apply {
homeworkBody = description
}
eventObject.attachmentIds = mutableListOf()
eventObject.attachmentNames = mutableListOf()
homework.getString("Attachments")?.split(',')?.onEach { url ->
val filename = "&filename=(.*?)&".toRegex().find(url)?.get(1) ?: return@onEach
val ext = "&ext=(.*?)&".toRegex().find(url)?.get(1) ?: return@onEach
eventObject.attachmentIds?.add(url.crc32())
eventObject.attachmentNames?.add("$filename.$ext:$url")
}
data.eventList.add(eventObject)
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_HOMEWORK,
id,
data.profile?.empty ?: false,
data.profile?.empty ?: false
))
}
data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK))
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-13
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.utils.models.Date
class PodlasieApiLuckyNumber(val data: DataPodlasie, val luckyNumber: Int) {
init {
val luckyNumberObject = LuckyNumber(
profileId = data.profileId,
date = Date.getToday(),
number = luckyNumber
)
data.luckyNumberList.add(luckyNumberObject)
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_LUCKY_NUMBER,
luckyNumberObject.date.value.toLong(),
true,
data.profile?.empty ?: false
))
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api
import pl.szczodrzynski.edziennik.asJsonObjectList
import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_USER_ENDPOINT
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.ENDPOINT_PODLASIE_API_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieApi
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getJsonArray
class PodlasieApiMain(override val data: DataPodlasie,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit) : PodlasieApi(data, lastSync) {
companion object {
const val TAG = "PodlasieApiTimetable"
}
init {
apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json ->
data.getTeam() // Save the class team when it doesn't exist.
json.getInt("LuckyNumber")?.let { PodlasieApiLuckyNumber(data, it) }
json.getJsonArray("Teacher")?.asJsonObjectList()?.let { PodlasieApiTeachers(data, it) }
json.getJsonArray("Timetable")?.asJsonObjectList()?.let { PodlasieApiTimetable(data, it) }
json.getJsonArray("Marks")?.asJsonObjectList()?.let { PodlasieApiGrades(data, it) }
json.getJsonArray("MarkFinal")?.asJsonObjectList()?.let { PodlasieApiFinalGrades(data, it) }
json.getJsonArray("News")?.asJsonObjectList()?.let { PodlasieApiEvents(data, it) }
json.getJsonArray("Tasks")?.asJsonObjectList()?.let { PodlasieApiHomework(data, it) }
data.setSyncNext(ENDPOINT_PODLASIE_API_MAIN, SYNC_ALWAYS)
onSuccess(ENDPOINT_PODLASIE_API_MAIN)
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-13
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
class PodlasieApiTeachers(val data: DataPodlasie, val rows: List<JsonObject>) {
init {
rows.forEach { teacher ->
val id = teacher.getLong("ExternalId") ?: return@forEach
val firstName = teacher.getString("FirstName") ?: return@forEach
val lastName = teacher.getString("LastName") ?: return@forEach
val isEducator = teacher.getInt("Educator") == 1
val teacherObject = Teacher(
profileId = data.profileId,
id = id,
name = firstName,
surname = lastName,
loginId = null
)
data.teacherList.put(id, teacherObject)
val teamClass = data.teamClass
if (isEducator && teamClass != null) {
data.teamList.put(teamClass.id, teamClass.apply {
teacherId = id
})
}
}
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class PodlasieApiTimetable(val data: DataPodlasie, rows: List<JsonObject>) {
init {
val currentWeekStart = Week.getWeekStart()
if (Date.getToday().weekDay > 4) {
currentWeekStart.stepForward(0, 0, 7)
}
val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d
val weekStart = Date.fromY_m_d(getDate)
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
val days = mutableListOf<Int>()
var startDate: Date? = null
var endDate: Date? = null
rows.forEach { lesson ->
val date = lesson.getString("Date")?.let { Date.fromY_m_d(it) } ?: return@forEach
if ((date > weekEnd || date < weekStart) && data.profile?.empty != true) return@forEach
if (startDate == null) startDate = date.clone()
endDate = date.clone()
if (date.value !in days) days += date.value
val lessonNumber = lesson.getInt("LessonNumber") ?: return@forEach
val startTime = lesson.getString("TimeFrom")?.let { Time.fromH_m_s(it) }
?: return@forEach
val endTime = lesson.getString("TimeTo")?.let { Time.fromH_m_s(it) } ?: return@forEach
val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(it) }
?: return@forEach
val teacherFirstName = lesson.getString("TeacherFirstName") ?: return@forEach
val teacherLastName = lesson.getString("TeacherLastName") ?: return@forEach
val teacher = data.getTeacher(teacherFirstName, teacherLastName)
val team = lesson.getString("Group")?.let { data.getTeam(it) } ?: return@forEach
val classroom = lesson.getString("Room")
Lesson(data.profileId, -1).also {
it.type = Lesson.TYPE_NORMAL
it.date = date
it.lessonNumber = lessonNumber
it.startTime = startTime
it.endTime = endTime
it.subjectId = subject.id
it.teacherId = teacher.id
it.teamId = team.id
it.classroom = classroom
it.id = it.buildId()
data.lessonList += it
}
}
if (startDate != null && endDate != null) {
if (weekEnd > endDate!!) endDate = weekEnd
while (startDate!! <= endDate!!) {
if (startDate!!.value !in days) {
val lessonDate = startDate!!.clone()
data.lessonList += Lesson(data.profileId, lessonDate.value.toLong()).apply {
type = Lesson.TYPE_NO_LESSONS
date = lessonDate
}
}
startDate!!.stepForward(0, 0, 1)
}
}
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_PODLASIE
import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_LOGOUT_DEVICES_ENDPOINT
import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_USER_ENDPOINT
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieApi
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLoginApi
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.db.entity.Profile
class PodlasieFirstLogin(val data: DataPodlasie, val onSuccess: () -> Unit) {
companion object {
const val TAG = "PodlasieFirstLogin"
}
private val api = PodlasieApi(data, null)
init {
PodlasieLoginApi(data) {
doLogin()
}
}
private fun doLogin() {
val loginStoreId = data.loginStore.id
val loginStoreType = LOGIN_TYPE_PODLASIE
if (data.loginStore.getLoginData("logoutDevices", false)) {
data.loginStore.removeLoginData("logoutDevices")
api.apiGet(TAG, PODLASIE_API_LOGOUT_DEVICES_ENDPOINT) {
doLogin()
}
return
}
api.apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json ->
val uuid = json.getString("Uuid")
val login = json.getString("Login")
val firstName = json.getString("FirstName")
val lastName = json.getString("LastName")
val studentNameLong = "$firstName $lastName".fixName()
val studentNameShort = studentNameLong.getShortName()
val schoolName = json.getString("SchoolName")
val className = json.getString("SchoolClass")
val schoolYear = json.getString("ActualSchoolYear")?.replace(' ', '/')
val semester = json.getString("ActualTermShortcut")?.length
val apiUrl = json.getString("URL")
val profile = Profile(
loginStoreId,
loginStoreId,
loginStoreType,
studentNameLong,
login,
studentNameLong,
studentNameShort,
null
).apply {
studentData["studentId"] = uuid
studentData["studentLogin"] = login
studentData["schoolName"] = schoolName
studentData["className"] = className
studentData["schoolYear"] = schoolYear
studentData["currentSemester"] = semester ?: 1
studentData["apiUrl"] = apiUrl
schoolYear?.split('/')?.get(0)?.toInt()?.let {
studentSchoolYearStart = it
}
studentClassName = className
}
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(profile), data.loginStore))
onSuccess()
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_PODLASIE_API
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.utils.Utils
class PodlasieLogin(val data: DataPodlasie, val onSuccess: () -> Unit) {
companion object {
const val TAG = "PodlasieLogin"
}
private var cancelled = false
init {
nextLoginMethod(onSuccess)
}
private fun nextLoginMethod(onSuccess: () -> Unit) {
if (data.targetLoginMethodIds.isEmpty()) {
onSuccess()
return
}
if (cancelled) {
onSuccess()
return
}
useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId ->
data.progress(data.progressStep)
if (usedMethodId != -1)
data.loginMethods.add(usedMethodId)
nextLoginMethod(onSuccess)
}
}
private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) {
// this should never be true
if (data.loginMethods.contains(loginMethodId)) {
onSuccess(-1)
return
}
Utils.d(TAG, "Using login method $loginMethodId")
when (loginMethodId) {
LOGIN_METHOD_PODLASIE_API -> {
data.startProgress(R.string.edziennik_progress_login_podlasie_api)
PodlasieLoginApi(data) { onSuccess(loginMethodId) }
}
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-5-12
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login
import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie
import pl.szczodrzynski.edziennik.data.api.models.ApiError
class PodlasieLoginApi(val data: DataPodlasie, val onSuccess: () -> Unit) {
companion object {
const val TAG = "PodlasieLoginApi"
}
init { run {
if (data.isApiLoginValid()) {
onSuccess()
} else {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
}
}}
}

View File

@ -100,7 +100,6 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
callback.onCompleted()
}
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {

View File

@ -4,34 +4,49 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.currentTimeUnix
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN
import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.values
import pl.szczodrzynski.fslogin.realm.RealmData
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
fun isWebMainLoginValid() = symbol.isNotNullNorEmpty()
&& (webExpiryTime[symbol]?.toLongOrNull() ?: 0) - 30 > currentTimeUnix()
&& webAuthCookie[symbol].isNotNullNorEmpty()
&& webRealmData != null
fun isApiLoginValid() = currentSemesterEndDate-30 > currentTimeUnix()
&& apiCertificateKey.isNotNullNorEmpty()
&& apiCertificatePrivate.isNotNullNorEmpty()
&& apiFingerprint[symbol].isNotNullNorEmpty()
&& apiPrivateKey[symbol].isNotNullNorEmpty()
&& symbol.isNotNullNorEmpty()
fun isHebeLoginValid() = hebePublicKey.isNotNullNorEmpty()
&& hebePrivateKey.isNotNullNorEmpty()
&& symbol.isNotNullNorEmpty()
override fun satisfyLoginMethods() {
loginMethods.clear()
if (isWebMainLoginValid()) {
loginMethods += LOGIN_METHOD_VULCAN_WEB_MAIN
}
if (isApiLoginValid()) {
loginMethods += LOGIN_METHOD_VULCAN_API
}
if (isHebeLoginValid()) {
loginMethods += LOGIN_METHOD_VULCAN_HEBE
}
}
init {
// during the first sync `profile.studentClassName` is already set
if (teamList.values().none { it.type == Team.TYPE_CLASS }) {
if (loginStore.mode == LOGIN_MODE_VULCAN_API
&& teamList.values().none { it.type == Team.TYPE_CLASS }) {
profile?.studentClassName?.also { name ->
val id = Utils.crc16(name.toByteArray()).toLong()
@ -40,7 +55,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
id,
name,
Team.TYPE_CLASS,
"$schoolName:$name",
"$schoolCode:$name",
-1
)
teamList.put(id, teamObject)
@ -48,7 +63,18 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
}
}
override fun generateUserCode() = "$schoolName:$studentId"
override fun generateUserCode() = "$schoolCode:$studentId"
fun buildDeviceId(): String {
val deviceId = app.deviceId.padStart(16, '0')
val loginStoreId = loginStore.id.toString(16).padStart(4, '0')
val symbol = symbol?.crc16()?.toString(16)?.take(2) ?: "00"
return deviceId.substring(0..7) +
"-" + deviceId.substring(8..11) +
"-" + deviceId.substring(12..15) +
"-" + loginStoreId +
"-" + symbol + "6f72616e7a"
}
/**
* A UONET+ client symbol.
@ -59,8 +85,8 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
*/
private var mSymbol: String? = null
var symbol: String?
get() { mSymbol = mSymbol ?: loginStore.getLoginData("deviceSymbol", null); return mSymbol }
set(value) { loginStore.putLoginData("deviceSymbol", value); mSymbol = value }
get() { mSymbol = mSymbol ?: profile?.getStudentData("symbol", null); return mSymbol }
set(value) { profile?.putStudentData("symbol", value); mSymbol = value }
/**
* Group symbol/number of the student's school.
@ -75,16 +101,26 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
set(value) { profile?.putStudentData("schoolSymbol", value) ?: return; mSchoolSymbol = value }
/**
* A school ID consisting of the [symbol] and [schoolSymbol].
* Short name of the school, used in some places.
*
* ListaUczniow/JednostkaSprawozdawczaSkrot, e.g. "SP Wilkow"
*/
private var mSchoolShort: String? = null
var schoolShort: String?
get() { mSchoolShort = mSchoolShort ?: profile?.getStudentData("schoolShort", null); return mSchoolShort }
set(value) { profile?.putStudentData("schoolShort", value) ?: return; mSchoolShort = value }
/**
* A school code consisting of the [symbol] and [schoolSymbol].
*
* [symbol]_[schoolSymbol]
*
* e.g. "poznan_000088"
*/
private var mSchoolName: String? = null
var schoolName: String?
get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName }
set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value }
private var mSchoolCode: String? = null
var schoolCode: String?
get() { mSchoolCode = mSchoolCode ?: profile?.getStudentData("schoolName", null); return mSchoolCode }
set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolCode = value }
/**
* ID of the student.
@ -124,6 +160,25 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
get() { mStudentSemesterId = mStudentSemesterId ?: profile?.getStudentData("studentSemesterId", 0); return mStudentSemesterId ?: 0 }
set(value) { profile?.putStudentData("studentSemesterId", value) ?: return; mStudentSemesterId = value }
private var mStudentUnitId: Int? = null
var studentUnitId: Int
get() { mStudentUnitId = mStudentUnitId ?: profile?.getStudentData("studentUnitId", 0); return mStudentUnitId ?: 0 }
set(value) { profile?.putStudentData("studentUnitId", value) ?: return; mStudentUnitId = value }
private var mStudentConstituentId: Int? = null
var studentConstituentId: Int
get() { mStudentConstituentId = mStudentConstituentId ?: profile?.getStudentData("studentConstituentId", 0); return mStudentConstituentId ?: 0 }
set(value) { profile?.putStudentData("studentConstituentId", value) ?: return; mStudentConstituentId = value }
private var mSemester1Id: Int? = null
var semester1Id: Int
get() { mSemester1Id = mSemester1Id ?: profile?.getStudentData("semester1Id", 0); return mSemester1Id ?: 0 }
set(value) { profile?.putStudentData("semester1Id", value) ?: return; mSemester1Id = value }
private var mSemester2Id: Int? = null
var semester2Id: Int
get() { mSemester2Id = mSemester2Id ?: profile?.getStudentData("semester2Id", 0); return mSemester2Id ?: 0 }
set(value) { profile?.putStudentData("semester2Id", value) ?: return; mSemester2Id = value }
/**
* ListaUczniow/OkresNumer, e.g. 1 or 2
*/
@ -154,45 +209,60 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
* After first login only 3 first characters are stored here.
* This is later used to determine the API URL address.
*/
private var mApiToken: String? = null
var apiToken: String?
get() { mApiToken = mApiToken ?: loginStore.getLoginData("deviceToken", null); return mApiToken }
set(value) { loginStore.putLoginData("deviceToken", value); mApiToken = value }
private var mApiToken: Map<String?, String?>? = null
var apiToken: Map<String?, String?> = mapOf()
get() { mApiToken = mApiToken ?: loginStore.getLoginData("apiToken", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiToken ?: mapOf() }
set(value) { loginStore.putLoginData("apiToken", app.gson.toJson(value)); mApiToken = value }
/**
* A mobile API registration PIN.
*
* After first login, this is removed and/or set to null.
*/
private var mApiPin: String? = null
var apiPin: String?
get() { mApiPin = mApiPin ?: loginStore.getLoginData("devicePin", null); return mApiPin }
set(value) { loginStore.putLoginData("devicePin", value); mApiPin = value }
private var mApiPin: Map<String?, String?>? = null
var apiPin: Map<String?, String?> = mapOf()
get() { mApiPin = mApiPin ?: loginStore.getLoginData("apiPin", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiPin ?: mapOf() }
set(value) { loginStore.putLoginData("apiPin", app.gson.toJson(value)); mApiPin = value }
private var mApiCertificateKey: String? = null
var apiCertificateKey: String?
get() { mApiCertificateKey = mApiCertificateKey ?: loginStore.getLoginData("certificateKey", null); return mApiCertificateKey }
set(value) { loginStore.putLoginData("certificateKey", value); mApiCertificateKey = value }
private var mApiFingerprint: Map<String?, String?>? = null
var apiFingerprint: Map<String?, String?> = mapOf()
get() { mApiFingerprint = mApiFingerprint ?: loginStore.getLoginData("apiFingerprint", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiFingerprint ?: mapOf() }
set(value) { loginStore.putLoginData("apiFingerprint", app.gson.toJson(value)); mApiFingerprint = value }
/**
* This is not meant for normal usage.
*
* It provides a backward compatibility (<4.0) in order
* to migrate and use private keys instead of PFX.
*/
private var mApiCertificatePfx: String? = null
var apiCertificatePfx: String?
get() { mApiCertificatePfx = mApiCertificatePfx ?: loginStore.getLoginData("certificatePfx", null); return mApiCertificatePfx }
set(value) { loginStore.putLoginData("certificatePfx", value); mApiCertificatePfx = value }
private var mApiPrivateKey: Map<String?, String?>? = null
var apiPrivateKey: Map<String?, String?> = mapOf()
get() { mApiPrivateKey = mApiPrivateKey ?: loginStore.getLoginData("apiPrivateKey", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiPrivateKey ?: mapOf() }
set(value) { loginStore.putLoginData("apiPrivateKey", app.gson.toJson(value)); mApiPrivateKey = value }
private var mApiCertificatePrivate: String? = null
var apiCertificatePrivate: String?
get() { mApiCertificatePrivate = mApiCertificatePrivate ?: loginStore.getLoginData("certificatePrivate", null); return mApiCertificatePrivate }
set(value) { loginStore.putLoginData("certificatePrivate", value); mApiCertificatePrivate = value }
/* _ _ _ _____ _____
| | | | | | /\ | __ \_ _|
| |__| | ___| |__ ___ / \ | |__) || |
| __ |/ _ \ '_ \ / _ \ / /\ \ | ___/ | |
| | | | __/ |_) | __/ / ____ \| | _| |_
|_| |_|\___|_.__/ \___| /_/ \_\_| |____*/
private var mHebePublicKey: String? = null
var hebePublicKey: String?
get() { mHebePublicKey = mHebePublicKey ?: loginStore.getLoginData("hebePublicKey", null); return mHebePublicKey }
set(value) { loginStore.putLoginData("hebePublicKey", value); mHebePublicKey = value }
private var mHebePrivateKey: String? = null
var hebePrivateKey: String?
get() { mHebePrivateKey = mHebePrivateKey ?: loginStore.getLoginData("hebePrivateKey", null); return mHebePrivateKey }
set(value) { loginStore.putLoginData("hebePrivateKey", value); mHebePrivateKey = value }
private var mHebePublicHash: String? = null
var hebePublicHash: String?
get() { mHebePublicHash = mHebePublicHash ?: loginStore.getLoginData("hebePublicHash", null); return mHebePublicHash }
set(value) { loginStore.putLoginData("hebePublicHash", value); mHebePublicHash = value }
private var mHebeContext: String? = null
var hebeContext: String?
get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext }
set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value }
val apiUrl: String?
get() {
val url = when (apiToken?.substring(0, 3)) {
val url = when (apiToken[symbol]?.substring(0, 3)) {
"3S1" -> "https://lekcjaplus.vulcan.net.pl"
"TA1" -> "https://uonetplus-komunikacja.umt.tarnow.pl"
"OP1" -> "https://uonetplus-komunikacja.eszkola.opolskie.pl"
@ -206,6 +276,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
"P01" -> "http://efeb-komunikacja.pro-hudson.win.vulcan.pl"
"P02" -> "http://efeb-komunikacja.pro-hudsonrc.win.vulcan.pl"
"P90" -> "http://efeb-komunikacja-pro-mwujakowska.neo.win.vulcan.pl"
"KO1" -> "https://uonetplus-komunikacja.eduportal.koszalin.pl"
"FK1", "FS1" -> "http://api.fakelog.cf"
"SZ9" -> "http://hack.szkolny.eu"
else -> null
@ -213,8 +284,65 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
return if (url != null) "$url/$symbol/" else loginStore.getLoginData("apiUrl", null)
}
val fullApiUrl: String?
val fullApiUrl: String
get() {
return "$apiUrl$schoolSymbol/"
}
/* __ __ _ ______ _____ _ _
\ \ / / | | | ____/ ____| | | (_)
\ \ /\ / /__| |__ | |__ | (___ | | ___ __ _ _ _ __
\ \/ \/ / _ \ '_ \ | __| \___ \ | | / _ \ / _` | | '_ \
\ /\ / __/ |_) | | | ____) | | |___| (_) | (_| | | | | |
\/ \/ \___|_.__/ |_| |_____/ |______\___/ \__, |_|_| |_|
__/ |
|__*/
var webRealmData: RealmData?
get() { mWebRealmData = mWebRealmData ?: loginStore.getLoginData("webRealmData", JsonObject()).let {
app.gson.fromJson(it, RealmData::class.java)
}; return mWebRealmData }
set(value) { loginStore.putLoginData("webRealmData", app.gson.toJsonTree(value) as JsonObject); mWebRealmData = value }
private var mWebRealmData: RealmData? = null
val webHost
get() = webRealmData?.host
var webEmail: String?
get() { mWebEmail = mWebEmail ?: loginStore.getLoginData("webEmail", null); return mWebEmail }
set(value) { loginStore.putLoginData("webEmail", value); mWebEmail = value }
private var mWebEmail: String? = null
var webUsername: String?
get() { mWebUsername = mWebUsername ?: loginStore.getLoginData("webUsername", null); return mWebUsername }
set(value) { loginStore.putLoginData("webUsername", value); mWebUsername = value }
private var mWebUsername: String? = null
var webPassword: String?
get() { mWebPassword = mWebPassword ?: loginStore.getLoginData("webPassword", null); return mWebPassword }
set(value) { loginStore.putLoginData("webPassword", value); mWebPassword = value }
private var mWebPassword: String? = null
/**
* Expiry time of a certificate POSTed to a LoginEndpoint of the specific symbol.
* If the time passes, the certificate needs to be POSTed again (if valid)
* or re-generated.
*/
var webExpiryTime: Map<String, String?> = mapOf()
get() { mWebExpiryTime = mWebExpiryTime ?: loginStore.getLoginData("webExpiryTime", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mWebExpiryTime ?: mapOf() }
set(value) { loginStore.putLoginData("webExpiryTime", app.gson.toJson(value)); mWebExpiryTime = value }
private var mWebExpiryTime: Map<String, String?>? = null
/**
* EfebSsoAuthCookie retrieved after posting a certificate
*/
var webAuthCookie: Map<String, String?> = mapOf()
get() { mWebAuthCookie = mWebAuthCookie ?: loginStore.getLoginData("webAuthCookie", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mWebAuthCookie ?: mapOf() }
set(value) { loginStore.putLoginData("webAuthCookie", app.gson.toJson(value)); mWebAuthCookie = value }
private var mWebAuthCookie: Map<String, String?>? = null
/**
* Permissions needed to get JSONs from home page
*/
var webPermissions: Map<String, String?> = mapOf()
get() { mWebPermissions = mWebPermissions ?: loginStore.getLoginData("webPermissions", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mWebPermissions ?: mapOf() }
set(value) { loginStore.putLoginData("webPermissions", app.gson.toJson(value)); mWebPermissions = value }
private var mWebPermissions: Map<String, String?>? = null
}

View File

@ -13,6 +13,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanData
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiAttachments
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiMessagesChangeStatus
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.VulcanApiSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeMessagesChangeStatus
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeSendMessage
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin.VulcanFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
@ -91,6 +93,20 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun getMessage(message: MessageFull) {
if (loginStore.mode != LOGIN_MODE_VULCAN_API) {
login(LOGIN_METHOD_VULCAN_HEBE) {
if (message.seen) {
EventBus.getDefault().postSticky(MessageGetEvent(message))
completed()
return@login
}
VulcanHebeMessagesChangeStatus(data, message) {
completed()
}
}
return
}
login(LOGIN_METHOD_VULCAN_API) {
if (message.attachmentIds != null) {
VulcanApiMessagesChangeStatus(data, message) {
@ -120,6 +136,15 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun sendMessage(recipients: List<Teacher>, subject: String, text: String) {
if (loginStore.mode != LOGIN_MODE_VULCAN_API) {
login(LOGIN_METHOD_VULCAN_HEBE) {
VulcanHebeSendMessage(data, recipients, subject, text) {
completed()
}
}
return
}
login(LOGIN_METHOD_VULCAN_API) {
VulcanApiSendMessage(data, recipients, subject, text) {
completed()
@ -157,7 +182,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
completed()
},
onProgress = { written, total ->
onProgress = { written, _ ->
val event = AttachmentGetEvent(
data.profileId,
owner,
@ -175,6 +200,14 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun getEvent(eventFull: EventFull) {
if (loginStore.mode != LOGIN_MODE_VULCAN_API) {
eventFull.homeworkBody = ""
EventBus.getDefault().postSticky(EventGetEvent(eventFull))
completed()
return
}
login(LOGIN_METHOD_VULCAN_API) {
val list = data.app.db.eventDao().getAllNow(data.profileId).filter { !it.addedManually }
VulcanApiAttachments(data, list, eventFull, EventFull::class) { _ ->
@ -194,7 +227,6 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
callback.onCompleted()
}
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {

View File

@ -20,32 +20,67 @@ const val ENDPOINT_VULCAN_API_ATTENDANCE = 1080
const val ENDPOINT_VULCAN_API_MESSAGES_INBOX = 1090
const val ENDPOINT_VULCAN_API_MESSAGES_SENT = 1100
const val ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS = 2010
const val ENDPOINT_VULCAN_HEBE_MAIN = 3000
const val ENDPOINT_VULCAN_HEBE_PUSH_CONFIG = 3005
const val ENDPOINT_VULCAN_HEBE_ADDRESSBOOK = 3010
const val ENDPOINT_VULCAN_HEBE_TIMETABLE = 3020
const val ENDPOINT_VULCAN_HEBE_EXAMS = 3030
const val ENDPOINT_VULCAN_HEBE_GRADES = 3040
const val ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY = 3050
const val ENDPOINT_VULCAN_HEBE_HOMEWORK = 3060
const val ENDPOINT_VULCAN_HEBE_NOTICES = 3070
const val ENDPOINT_VULCAN_HEBE_ATTENDANCE = 3080
const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3090
const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3100
const val ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER = 3200
val VulcanFeatures = listOf(
// timetable
Feature(LOGIN_TYPE_VULCAN, FEATURE_TIMETABLE, listOf(
ENDPOINT_VULCAN_API_TIMETABLE to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_TIMETABLE, listOf(
ENDPOINT_VULCAN_HEBE_TIMETABLE to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// agenda
Feature(LOGIN_TYPE_VULCAN, FEATURE_AGENDA, listOf(
ENDPOINT_VULCAN_API_EVENTS to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_AGENDA, listOf(
ENDPOINT_VULCAN_HEBE_EXAMS to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// grades
Feature(LOGIN_TYPE_VULCAN, FEATURE_GRADES, listOf(
ENDPOINT_VULCAN_API_GRADES to LOGIN_METHOD_VULCAN_API,
ENDPOINT_VULCAN_API_GRADES_SUMMARY to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_GRADES, listOf(
ENDPOINT_VULCAN_HEBE_GRADES to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// homework
Feature(LOGIN_TYPE_VULCAN, FEATURE_HOMEWORK, listOf(
ENDPOINT_VULCAN_API_HOMEWORK to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_HOMEWORK, listOf(
ENDPOINT_VULCAN_HEBE_HOMEWORK to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// behaviour
Feature(LOGIN_TYPE_VULCAN, FEATURE_BEHAVIOUR, listOf(
ENDPOINT_VULCAN_API_NOTICES to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_BEHAVIOUR, listOf(
ENDPOINT_VULCAN_HEBE_NOTICES to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// attendance
Feature(LOGIN_TYPE_VULCAN, FEATURE_ATTENDANCE, listOf(
ENDPOINT_VULCAN_API_ATTENDANCE to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_ATTENDANCE, listOf(
ENDPOINT_VULCAN_HEBE_ATTENDANCE to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// messages
Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_INBOX, listOf(
ENDPOINT_VULCAN_API_MESSAGES_INBOX to LOGIN_METHOD_VULCAN_API
@ -53,6 +88,12 @@ val VulcanFeatures = listOf(
Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_SENT, listOf(
ENDPOINT_VULCAN_API_MESSAGES_SENT to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_INBOX, listOf(
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_SENT, listOf(
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)),
// push config
Feature(LOGIN_TYPE_VULCAN, FEATURE_PUSH_CONFIG, listOf(
@ -60,11 +101,37 @@ val VulcanFeatures = listOf(
), listOf(LOGIN_METHOD_VULCAN_API)).withShouldSync { data ->
!data.app.config.sync.tokenVulcanList.contains(data.profileId)
},
Feature(LOGIN_TYPE_VULCAN, FEATURE_PUSH_CONFIG, listOf(
ENDPOINT_VULCAN_HEBE_PUSH_CONFIG to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE)).withShouldSync { data ->
!data.app.config.sync.tokenVulcanList.contains(data.profileId)
},
/**
* Lucky number - using WEB Main.
*/
Feature(LOGIN_TYPE_VULCAN, FEATURE_LUCKY_NUMBER, listOf(
ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS to LOGIN_METHOD_VULCAN_WEB_MAIN
), listOf(LOGIN_METHOD_VULCAN_WEB_MAIN))
.withShouldSync { data -> data.shouldSyncLuckyNumber() }
.withPriority(2),
/**
* Lucky number - using Hebe API
*/
Feature(LOGIN_TYPE_VULCAN, FEATURE_LUCKY_NUMBER, listOf(
ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE))
.withShouldSync { data -> data.shouldSyncLuckyNumber() }
.withPriority(1),
Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf(
ENDPOINT_VULCAN_API_UPDATE_SEMESTER to LOGIN_METHOD_VULCAN_API,
ENDPOINT_VULCAN_API_DICTIONARIES to LOGIN_METHOD_VULCAN_API
), listOf(LOGIN_METHOD_VULCAN_API))
), listOf(LOGIN_METHOD_VULCAN_API)),
Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf(
ENDPOINT_VULCAN_HEBE_MAIN to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LOGIN_METHOD_VULCAN_HEBE
), listOf(LOGIN_METHOD_VULCAN_HEBE))
/*Feature(LOGIN_TYPE_VULCAN, FEATURE_STUDENT_INFO, listOf(
ENDPOINT_VULCAN_API to LOGIN_METHOD_VULCAN_WEB
), listOf(LOGIN_METHOD_VULCAN_WEB)),

View File

@ -106,11 +106,11 @@ open class VulcanApi(open val data: DataVulcan, open val lastSync: Long?) {
Request.builder()
.url(url)
.userAgent(VULCAN_API_USER_AGENT)
.addHeader("RequestCertificateKey", data.apiCertificateKey)
.addHeader("RequestCertificateKey", data.apiFingerprint[data.symbol])
.addHeader("RequestSignatureValue",
try {
signContent(
data.apiCertificatePrivate ?: "",
data.apiPrivateKey[data.symbol] ?: "",
finalPayload.toString()
)
} catch (e: Exception) {e.printStackTrace();""})

View File

@ -5,8 +5,13 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED
import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web.VulcanWebLuckyNumber
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.utils.Utils
class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
@ -14,9 +19,40 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
private const val TAG = "VulcanData"
}
init {
nextEndpoint(onSuccess)
}
private var firstSemesterSync = false
private val firstSemesterSyncExclude = listOf(
ENDPOINT_VULCAN_HEBE_MAIN,
ENDPOINT_VULCAN_HEBE_PUSH_CONFIG,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK,
ENDPOINT_VULCAN_HEBE_TIMETABLE,
ENDPOINT_VULCAN_HEBE_EXAMS,
ENDPOINT_VULCAN_HEBE_HOMEWORK,
ENDPOINT_VULCAN_HEBE_NOTICES,
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX,
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT,
ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER
)
init { run {
if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) {
data.error(TAG, ERROR_VULCAN_API_DEPRECATED)
return@run
}
if (data.studentSemesterNumber == 2 && data.profile?.empty != false) {
firstSemesterSync = true
// set to sync 1st semester first
data.studentSemesterId = data.semester1Id
data.studentSemesterNumber = 1
}
nextEndpoint {
if (firstSemesterSync) {
// at the end, set back 2nd semester
data.studentSemesterId = data.semester2Id
data.studentSemesterNumber = 2
}
onSuccess()
}
}}
private fun nextEndpoint(onSuccess: () -> Unit) {
if (data.targetEndpointIds.isEmpty()) {
@ -29,7 +65,21 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
}
val id = data.targetEndpointIds.firstKey()
val lastSync = data.targetEndpointIds.remove(id)
useEndpoint(id, lastSync) { endpointId ->
useEndpoint(id, lastSync) {
if (firstSemesterSync && id !in firstSemesterSyncExclude) {
// sync 2nd semester after every endpoint
data.studentSemesterId = data.semester2Id
data.studentSemesterNumber = 2
useEndpoint(id, lastSync) {
// set 1st semester back for the next endpoint
data.studentSemesterId = data.semester1Id
data.studentSemesterNumber = 1
// progress further
data.progress(data.progressStep)
nextEndpoint(onSuccess)
}
return@useEndpoint
}
data.progress(data.progressStep)
nextEndpoint(onSuccess)
}
@ -86,6 +136,71 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox)
VulcanApiMessagesSent(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS -> {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
VulcanWebLuckyNumber(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_MAIN -> {
if (data.profile == null) {
onSuccess(ENDPOINT_VULCAN_HEBE_MAIN)
return
}
data.startProgress(R.string.edziennik_progress_endpoint_student_info)
VulcanHebeMain(data, lastSync).getStudents(
profile = data.profile,
profileList = null
) {
onSuccess(ENDPOINT_VULCAN_HEBE_MAIN)
}
}
ENDPOINT_VULCAN_HEBE_PUSH_CONFIG -> {
data.startProgress(R.string.edziennik_progress_endpoint_push_config)
VulcanHebePushConfig(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK -> {
data.startProgress(R.string.edziennik_progress_endpoint_teachers)
VulcanHebeAddressbook(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_TIMETABLE -> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
VulcanHebeTimetable(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_EXAMS -> {
data.startProgress(R.string.edziennik_progress_endpoint_exams)
VulcanHebeExams(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_GRADES -> {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
VulcanHebeGrades(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY -> {
data.startProgress(R.string.edziennik_progress_endpoint_proposed_grades)
VulcanHebeGradeSummary(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_HOMEWORK -> {
data.startProgress(R.string.edziennik_progress_endpoint_homework)
VulcanHebeHomework(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_NOTICES -> {
data.startProgress(R.string.edziennik_progress_endpoint_notices)
VulcanHebeNotices(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_ATTENDANCE -> {
data.startProgress(R.string.edziennik_progress_endpoint_attendance)
VulcanHebeAttendance(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_RECEIVED)
}
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_outbox)
VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_SENT)
}
ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER -> {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
VulcanHebeLuckyNumber(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}

View File

@ -0,0 +1,393 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data
import android.os.Build
import androidx.core.util.set
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.JsonCallbackHandler
import io.github.wulkanowy.signer.hebe.getSignatureHeaders
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.HebeFilterType
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.LessonRange
import pl.szczodrzynski.edziennik.data.db.entity.Subject
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.net.HttpURLConnection
import java.net.URLEncoder
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.*
open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
companion object {
const val TAG = "VulcanHebe"
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
fun getDateTime(
json: JsonObject?,
key: String,
default: Long = System.currentTimeMillis()
): Long {
val date = json.getJsonObject(key)
return date.getLong("Timestamp") ?: return default
}
fun getDate(json: JsonObject?, key: String): Date? {
val date = json.getJsonObject(key)
return date.getString("Date")?.let { Date.fromY_m_d(it) }
}
fun getTeacherId(json: JsonObject?, key: String): Long? {
val teacher = json.getJsonObject(key)
val teacherId = teacher.getLong("Id") ?: return null
if (data.teacherList[teacherId] == null) {
data.teacherList[teacherId] = Teacher(
data.profileId,
teacherId,
teacher.getString("Name") ?: "",
teacher.getString("Surname") ?: ""
)
}
return teacherId
}
fun getSubjectId(json: JsonObject?, key: String): Long? {
val subject = json.getJsonObject(key)
val subjectId = subject.getLong("Id") ?: return null
if (data.subjectList[subjectId] == null) {
data.subjectList[subjectId] = Subject(
data.profileId,
subjectId,
subject.getString("Name") ?: "",
subject.getString("Kod") ?: ""
)
}
return subjectId
}
fun getTeamId(json: JsonObject?, key: String): Long? {
val team = json.getJsonObject(key)
val teamId = team.getLong("Id") ?: return null
if (data.teamList[teamId] == null) {
var name = team.getString("Shortcut")
?: team.getString("Name")
?: ""
name = "${profile?.studentClassName ?: ""} $name"
data.teamList[teamId] = Team(
data.profileId,
teamId,
name,
Team.TYPE_VIRTUAL,
"${data.schoolCode}:$name",
-1
)
}
return teamId
}
fun getClassId(json: JsonObject?, key: String): Long? {
val team = json.getJsonObject(key)
val teamId = team.getLong("Id") ?: return null
if (data.teamList[teamId] == null) {
val name = data.profile?.studentClassName
?: team.getString("Name")
?: ""
data.teamList[teamId] = Team(
data.profileId,
teamId,
name,
Team.TYPE_CLASS,
"${data.schoolCode}:$name",
-1
)
}
return teamId
}
fun getLessonRange(json: JsonObject?, key: String): LessonRange? {
val timeslot = json.getJsonObject(key)
val position = timeslot.getInt("Position") ?: return null
val start = timeslot.getString("Start") ?: return null
val end = timeslot.getString("End") ?: return null
val lessonRange = LessonRange(
data.profileId,
position,
Time.fromH_m(start),
Time.fromH_m(end)
)
data.lessonRanges[position] = lessonRange
return lessonRange
}
fun getSemester(json: JsonObject?): Int {
val periodId = json.getInt("PeriodId") ?: return 1
return if (periodId == data.semester1Id)
1
else
2
}
fun isCurrentYear(date: Date): Boolean {
return profile?.let { profile ->
return@let date >= profile.dateSemester1Start
} ?: false
}
fun isCurrentYear(dateTime: Long): Boolean {
return profile?.let { profile ->
return@let dateTime >= profile.dateSemester1Start.inMillis
} ?: false
}
inline fun <reified T> apiRequest(
tag: String,
endpoint: String,
method: Int = GET,
payload: JsonElement? = null,
baseUrl: Boolean = false,
firebaseToken: String? = null,
crossinline onSuccess: (json: T, response: Response?) -> Unit
) {
val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint"
d(tag, "Request: Vulcan/Hebe - $url")
val privateKey = data.hebePrivateKey
val publicHash = data.hebePublicHash
if (privateKey == null || publicHash == null) {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
return
}
val timestamp = ZonedDateTime.now(ZoneId.of("GMT"))
val timestampMillis = timestamp.toInstant().toEpochMilli()
val timestampIso = timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"))
val finalPayload = if (payload != null) {
JsonObject(
"AppName" to VULCAN_HEBE_APP_NAME,
"AppVersion" to VULCAN_HEBE_APP_VERSION,
"CertificateId" to publicHash,
"Envelope" to payload,
"FirebaseToken" to (firebaseToken ?: data.app.config.sync.tokenVulcanHebe),
"API" to 1,
"RequestId" to UUID.randomUUID().toString(),
"Timestamp" to timestampMillis,
"TimestampFormatted" to timestampIso
)
} else null
val jsonString = finalPayload?.toString()
val headers = getSignatureHeaders(
publicHash,
privateKey,
jsonString,
endpoint,
timestamp
)
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null) {
data.error(
ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response)
)
return
}
val status = json.getJsonObject("Status")
if (status?.getInt("Code") != 0) {
data.error(
ApiError(tag, ERROR_VULCAN_HEBE_OTHER)
.withResponse(response)
.withApiResponse(json.toString())
)
}
val envelope = if (json.get("Envelope").isJsonNull && null is T)
null as T
else when (T::class.java) {
JsonObject::class.java -> json.getJsonObject("Envelope") as T
JsonArray::class.java -> json.getJsonArray("Envelope") as T
java.lang.Boolean::class.java -> json.getBoolean("Envelope") as T
else -> {
data.error(
ApiError(tag, ERROR_RESPONSE_EMPTY)
.withResponse(response)
.withApiResponse(json)
)
return
}
}
try {
onSuccess(envelope, response)
} catch (e: Exception) {
data.error(
ApiError(tag, EXCEPTION_VULCAN_HEBE_REQUEST)
.withResponse(response)
.withThrowable(e)
.withApiResponse(json)
)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(
ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable)
)
}
}
Request.builder()
.url(url)
.userAgent(VULCAN_HEBE_USER_AGENT)
.addHeader("vOS", "Android")
.addHeader("vDeviceModel", Build.MODEL)
.addHeader("vAPI", "1")
.apply {
if (data.hebeContext != null)
addHeader("vContext", data.hebeContext)
headers.forEach {
addHeader(it.key, it.value)
}
when (method) {
GET -> get()
POST -> {
post()
setTextBody(jsonString, MediaTypeUtils.APPLICATION_JSON)
}
}
}
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN)
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
.allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE)
.callback(callback)
.build()
.enqueue()
}
inline fun <reified T> apiGet(
tag: String,
endpoint: String,
query: Map<String, String> = mapOf(),
baseUrl: Boolean = false,
firebaseToken: String? = null,
crossinline onSuccess: (json: T, response: Response?) -> Unit
) {
val queryPath = query.map {
it.key + "=" + URLEncoder.encode(it.value, "UTF-8").replace("+", "%20")
}.join("&")
apiRequest(
tag,
if (query.isNotEmpty()) "$endpoint?$queryPath" else endpoint,
baseUrl = baseUrl,
firebaseToken = firebaseToken,
onSuccess = onSuccess
)
}
inline fun <reified T> apiPost(
tag: String,
endpoint: String,
payload: JsonElement,
baseUrl: Boolean = false,
firebaseToken: String? = null,
crossinline onSuccess: (json: T, response: Response?) -> Unit
) {
apiRequest(
tag,
endpoint,
method = POST,
payload,
baseUrl = baseUrl,
firebaseToken = firebaseToken,
onSuccess = onSuccess
)
}
fun apiGetList(
tag: String,
endpoint: String,
filterType: HebeFilterType? = null,
dateFrom: Date? = null,
dateTo: Date? = null,
lastSync: Long? = null,
folder: Int? = null,
params: Map<String, String> = mapOf(),
includeFilterType: Boolean = true,
onSuccess: (data: List<JsonObject>, response: Response?) -> Unit
) {
val url = if (includeFilterType && filterType != null)
"$endpoint/${filterType.endpoint}"
else endpoint
val query = params.toMutableMap()
when (filterType) {
HebeFilterType.BY_PUPIL -> {
query["unitId"] = data.studentUnitId.toString()
query["pupilId"] = data.studentId.toString()
query["periodId"] = data.studentSemesterId.toString()
}
HebeFilterType.BY_PERSON -> {
query["loginId"] = data.studentLoginId.toString()
}
HebeFilterType.BY_PERIOD -> {
query["periodId"] = data.studentSemesterId.toString()
query["pupilId"] = data.studentId.toString()
}
}
if (dateFrom != null)
query["dateFrom"] = dateFrom.stringY_m_d
if (dateTo != null)
query["dateTo"] = dateTo.stringY_m_d
if (folder != null)
query["folder"] = folder.toString()
val semester1Start = profile?.dateSemester1Start?.inMillis
query["lastId"] = "-2147483648" // don't ask, it's just Vulcan
query["pageSize"] = "500"
query["lastSyncDate"] = LocalDateTime
.ofInstant(
Instant.ofEpochMilli(lastSync ?: semester1Start ?: 0),
ZoneId.systemDefault()
)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
apiGet(tag, url, query) { json: JsonArray, response ->
onSuccess(json.map { it.asJsonObject }, response)
}
}
}

View File

@ -0,0 +1,315 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-17.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.droidsonroids.jspoon.Jspoon
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
import java.io.File
import java.net.HttpURLConnection
open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) {
companion object {
const val TAG = "VulcanWebMain"
const val WEB_MAIN = 0
const val WEB_OLD = 1
const val WEB_NEW = 2
const val WEB_MESSAGES = 3
const val STATE_SUCCESS = 0
const val STATE_NO_REGISTER = 1
const val STATE_LOGGED_OUT = 2
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
private val certificateAdapter by lazy {
Jspoon.create().adapter(CufsCertificate::class.java)
}
fun saveCertificate(xml: String) {
val file = File(data.app.filesDir, "cert_"+(data.webUsername ?: data.webEmail)+".xml")
file.writeText(xml)
}
fun readCertificate(): String? {
val file = File(data.app.filesDir, "cert_"+(data.webUsername ?: data.webEmail)+".xml")
if (file.canRead())
return file.readText()
return null
}
fun parseCertificate(xml: String): CufsCertificate {
val xmlParsed = xml
.replace("<[a-z]+?:".toRegex(), "<")
.replace("</[a-z]+?:".toRegex(), "</")
.replace("\\sxmlns.*?=\".+?\"".toRegex(), "")
return certificateAdapter.fromHtml(xmlParsed).also {
it.xml = xml
}
}
fun postCertificate(certificate: CufsCertificate, symbol: String, onResult: (symbol: String, state: Int) -> Unit): Boolean {
// check if the certificate is valid
if (Date.fromIso(certificate.expiryDate) < System.currentTimeMillis())
return false
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (response?.headers()?.get("Location")?.contains("LoginEndpoint.aspx") == true
|| response?.headers()?.get("Location")?.contains("?logout=true") == true) {
onResult(symbol, STATE_LOGGED_OUT)
return
}
if (text?.contains("LoginEndpoint.aspx?logout=true") == true) {
onResult(symbol, STATE_NO_REGISTER)
return
}
if (!validateCallback(symbol, text, response, jsonResponse = false)) {
return
}
data.webExpiryTime = data.webExpiryTime.toMutableMap().also { map ->
map[symbol] = (Date.fromIso(certificate.expiryDate) / 1000L).toString()
}
onResult(symbol, STATE_SUCCESS)
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("https://uonetplus.${data.webHost}/$symbol/LoginEndpoint.aspx")
.withClient(data.app.httpLazy)
.userAgent(SYSTEM_USER_AGENT)
.post()
.addParameter("wa", "wsignin1.0")
.addParameter("wctx", certificate.targetUrl)
.addParameter("wresult", certificate.xml)
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN)
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
.allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE)
.allowErrorCode(429)
.callback(callback)
.build()
.enqueue()
return true
}
fun getStartPage(symbol: String = data.symbol ?: "default", postErrors: Boolean = true, onSuccess: (html: String, schoolSymbols: List<String>) -> Unit) {
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (!validateCallback(symbol, text, response, jsonResponse = false) || text == null) {
return
}
if (postErrors) {
when {
text.contains("status absolwenta") -> ERROR_VULCAN_WEB_GRADUATE_ACCOUNT
else -> null
}?.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withResponse(response)
.withApiResponse(text))
return
}
}
data.webPermissions = data.webPermissions.toMutableMap().also { map ->
val permissions = Regexes.VULCAN_WEB_PERMISSIONS.find(text)?.let { it[1] }
if (permissions?.isNotBlank() == true) {
val studentId = permissions.split("|")
.getOrNull(0)
?.base64DecodeToString()
?.toJsonObject()
?.getJsonArray("AuthInfos")
?.asJsonObjectList()
?.flatMap { authInfo ->
authInfo.getJsonArray("UczenIds")
?.map { it.asInt }
?: listOf()
}
?.firstOrNull()
?.toString()
data.app.cookieJar.set(
data.webHost ?: "vulcan.net.pl",
"idBiezacyUczen",
studentId
)
}
map[symbol] = permissions
}
val schoolSymbols = mutableListOf<String>()
val clientUrl = "://uonetplus-uczen.${data.webHost}/$symbol/"
var clientIndex = text.indexOf(clientUrl)
var count = 0
while (clientIndex != -1 && count < 100) {
val startIndex = clientIndex + clientUrl.length
val endIndex = text.indexOf('"', startIndex = startIndex)
val schoolSymbol = text.substring(startIndex, endIndex)
schoolSymbols += schoolSymbol
clientIndex = text.indexOf(clientUrl, startIndex = endIndex)
count++
}
schoolSymbols.removeAll {
it.toLowerCase() == "default"
|| !it.matches(Regexes.VULCAN_WEB_SYMBOL_VALIDATE)
}
if (postErrors && schoolSymbols.isEmpty()) {
data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_SCHOOLS)
.withResponse(response)
.withApiResponse(text))
return
}
onSuccess(text, schoolSymbols)
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("https://uonetplus.${data.webHost}/$symbol/Start.mvc/Index")
.userAgent(SYSTEM_USER_AGENT)
.get()
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN)
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
.allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE)
.allowErrorCode(429)
.callback(callback)
.build()
.enqueue()
}
private fun validateCallback(symbol: String, text: String?, response: Response?, jsonResponse: Boolean = true): Boolean {
if (text == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return false
}
if (response?.code() !in 200..302 || (jsonResponse && !text.startsWith("{"))) {
when {
text.contains("The custom error module") -> ERROR_VULCAN_WEB_429
else -> ERROR_VULCAN_WEB_OTHER
}.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withApiResponse(text)
.withResponse(response))
return false
}
}
val cookies = data.app.cookieJar.getAll(data.webHost ?: "vulcan.net.pl")
val authCookie = cookies["EfebSsoAuthCookie"]
if ((authCookie == null || authCookie == "null") && data.webAuthCookie[symbol] != null) {
data.app.cookieJar.set(data.webHost ?: "vulcan.net.pl", "EfebSsoAuthCookie", data.webAuthCookie[symbol])
}
else if (authCookie.isNotNullNorBlank() && authCookie != "null" && authCookie != data.webAuthCookie[symbol]) {
data.webAuthCookie = data.webAuthCookie.toMutableMap().also { map ->
map[symbol] = authCookie
}
}
return true
}
fun webGetJson(
tag: String,
webType: Int,
endpoint: String,
method: Int = POST,
parameters: Map<String, Any?> = emptyMap(),
onSuccess: (json: JsonObject, response: Response?) -> Unit
) {
val url = "https://" + when (webType) {
WEB_MAIN -> "uonetplus"
WEB_OLD -> "uonetplus-opiekun"
WEB_NEW -> "uonetplus-uczen"
WEB_MESSAGES -> "uonetplus-uzytkownik"
else -> "uonetplus"
} + ".${data.webHost}/${data.symbol}/$endpoint"
Utils.d(tag, "Request: Vulcan/WebMain - $url")
val payload = JsonObject()
parameters.map { (name, value) ->
when (value) {
is JsonObject -> payload.add(name, value)
is JsonArray -> payload.add(name, value)
is String -> payload.addProperty(name, value)
is Int -> payload.addProperty(name, value)
is Long -> payload.addProperty(name, value)
is Float -> payload.addProperty(name, value)
is Char -> payload.addProperty(name, value)
is Boolean -> payload.addProperty(name, value)
}
}
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (!validateCallback(data.symbol ?: "default", text, response))
return
try {
val json = JsonParser().parse(text).asJsonObject
onSuccess(json, response)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_VULCAN_WEB_REQUEST)
.withResponse(response)
.withThrowable(e)
.withApiResponse(text))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url(url)
.userAgent(SYSTEM_USER_AGENT)
.apply {
when (method) {
GET -> get()
POST -> post()
}
}
.setJsonBody(payload)
.allowErrorCode(429)
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_PRESENT
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.utils.models.Date
@ -25,15 +26,38 @@ class VulcanApiAttendance(override val data: DataVulcan,
data.db.attendanceTypeDao().getAllNow(profileId).toSparseArray(data.attendanceTypes) { it.id }
}
val startDate: String = profile.getSemesterStart(profile.currentSemester).stringY_m_d
val endDate: String = profile.getSemesterEnd(profile.currentSemester).stringY_m_d
val semesterId = data.studentSemesterId
val semesterNumber = data.studentSemesterNumber
if (semesterNumber == 2 && lastSync ?: 0 < profile.dateSemester1Start.inMillis) {
getAttendance(profile, semesterId - 1, semesterNumber - 1) {
getAttendance(profile, semesterId, semesterNumber) {
finish()
}
}
}
else {
getAttendance(profile, semesterId, semesterNumber) {
finish()
}
}
} ?: onSuccess(ENDPOINT_VULCAN_API_ATTENDANCE) }
private fun finish() {
data.setSyncNext(ENDPOINT_VULCAN_API_ATTENDANCE, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_API_ATTENDANCE)
}
private fun getAttendance(profile: Profile, semesterId: Int, semesterNumber: Int, onSuccess: () -> Unit) {
val startDate = profile.getSemesterStart(semesterNumber).stringY_m_d
val endDate = profile.getSemesterEnd(semesterNumber).stringY_m_d
apiGet(TAG, VULCAN_API_ENDPOINT_ATTENDANCE, parameters = mapOf(
"DataPoczatkowa" to startDate,
"DataKoncowa" to endDate,
"IdOddzial" to data.studentClassId,
"IdUczen" to data.studentId,
"IdOkresKlasyfikacyjny" to data.studentSemesterId
"IdOkresKlasyfikacyjny" to semesterId
)) { json, _ ->
json.getJsonObject("Data")?.getJsonArray("Frekwencje")?.forEach { attendanceEl ->
val attendance = attendanceEl.asJsonObject
@ -47,7 +71,7 @@ class VulcanApiAttendance(override val data: DataVulcan,
val lessonDate = Date.fromMillis(lessonDateMillis)
val startTime = data.lessonRanges.get(attendance.getInt("Numer") ?: 0)?.startTime
val lessonSemester = profile.dateToSemester(lessonDate)
val lessonSemester = semesterNumber
val attendanceObject = Attendance(
profileId = profileId,
@ -65,6 +89,7 @@ class VulcanApiAttendance(override val data: DataVulcan,
addedDate = lessonDate.combineWith(startTime)
).also {
it.lessonNumber = attendance.getInt("Numer")
it.isCounted = it.baseType != Attendance.TYPE_RELEASED
}
data.attendanceList.add(attendanceObject)
@ -79,8 +104,7 @@ class VulcanApiAttendance(override val data: DataVulcan,
}
}
data.setSyncNext(ENDPOINT_VULCAN_API_ATTENDANCE, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_API_ATTENDANCE)
onSuccess()
}
} ?: onSuccess(ENDPOINT_VULCAN_API_ATTENDANCE) }
}
}

View File

@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getBoolean
import pl.szczodrzynski.edziennik.getJsonArray
@ -31,11 +32,43 @@ class VulcanApiEvents(override val data: DataVulcan,
init { data.profile?.also { profile ->
val startDate: String = when (profile.empty) {
true -> profile.getSemesterStart(profile.currentSemester).stringY_m_d
val semesterId = data.studentSemesterId
val semesterNumber = data.studentSemesterNumber
if (semesterNumber == 2 && lastSync ?: 0 < profile.dateSemester1Start.inMillis) {
getEvents(profile, semesterId - 1, semesterNumber - 1) {
getEvents(profile, semesterId, semesterNumber) {
finish()
}
}
}
else {
getEvents(profile, semesterId, semesterNumber) {
finish()
}
}
} ?: onSuccess(if (isHomework) ENDPOINT_VULCAN_API_HOMEWORK else ENDPOINT_VULCAN_API_EVENTS) }
private fun finish() {
when (isHomework) {
true -> {
data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK))
data.setSyncNext(ENDPOINT_VULCAN_API_HOMEWORK, SYNC_ALWAYS)
}
false -> {
data.toRemove.add(DataRemoveModel.Events.futureExceptType(Event.TYPE_HOMEWORK))
data.setSyncNext(ENDPOINT_VULCAN_API_EVENTS, SYNC_ALWAYS)
}
}
onSuccess(if (isHomework) ENDPOINT_VULCAN_API_HOMEWORK else ENDPOINT_VULCAN_API_EVENTS)
}
private fun getEvents(profile: Profile, semesterId: Int, semesterNumber: Int, onSuccess: () -> Unit) {
val startDate = when (profile.empty) {
true -> profile.getSemesterStart(semesterNumber).stringY_m_d
else -> Date.getToday().stepForward(0, -1, 0).stringY_m_d
}
val endDate: String = profile.getSemesterEnd(profile.currentSemester).stringY_m_d
val endDate = profile.getSemesterEnd(semesterNumber).stringY_m_d
val endpoint = when (isHomework) {
true -> VULCAN_API_ENDPOINT_HOMEWORK
@ -46,7 +79,7 @@ class VulcanApiEvents(override val data: DataVulcan,
"DataKoncowa" to endDate,
"IdOddzial" to data.studentClassId,
"IdUczen" to data.studentId,
"IdOkresKlasyfikacyjny" to data.studentSemesterId
"IdOkresKlasyfikacyjny" to semesterId
)) { json, _ ->
val events = json.getJsonArray("Data")
@ -94,17 +127,7 @@ class VulcanApiEvents(override val data: DataVulcan,
))
}
when (isHomework) {
true -> {
data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK))
data.setSyncNext(ENDPOINT_VULCAN_API_HOMEWORK, SYNC_ALWAYS)
}
false -> {
data.toRemove.add(DataRemoveModel.Events.futureExceptType(Event.TYPE_HOMEWORK))
data.setSyncNext(ENDPOINT_VULCAN_API_EVENTS, SYNC_ALWAYS)
}
}
onSuccess(if (isHomework) ENDPOINT_VULCAN_API_HOMEWORK else ENDPOINT_VULCAN_API_EVENTS)
onSuccess()
}
} ?: onSuccess(if (isHomework) ENDPOINT_VULCAN_API_HOMEWORK else ENDPOINT_VULCAN_API_EVENTS) }
}
}

View File

@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import java.text.DecimalFormat
import kotlin.math.roundToInt
@ -27,9 +28,33 @@ class VulcanApiGrades(override val data: DataVulcan,
init { data.profile?.also { profile ->
val semesterId = data.studentSemesterId
val semesterNumber = data.studentSemesterNumber
if (semesterNumber == 2 && lastSync ?: 0 < profile.dateSemester1Start.inMillis) {
getGrades(profile, semesterId - 1, semesterNumber - 1) {
getGrades(profile, semesterId, semesterNumber) {
finish()
}
}
}
else {
getGrades(profile, semesterId, semesterNumber) {
finish()
}
}
} ?: onSuccess(ENDPOINT_VULCAN_API_GRADES) }
private fun finish() {
data.toRemove.add(DataRemoveModel.Grades.semesterWithType(data.studentSemesterNumber, TYPE_NORMAL))
data.setSyncNext(ENDPOINT_VULCAN_API_GRADES, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_API_GRADES)
}
private fun getGrades(profile: Profile, semesterId: Int, semesterNumber: Int, onSuccess: () -> Unit) {
apiGet(TAG, VULCAN_API_ENDPOINT_GRADES, parameters = mapOf(
"IdUczen" to data.studentId,
"IdOkresKlasyfikacyjny" to data.studentSemesterId
"IdOkresKlasyfikacyjny" to semesterId
)) { json, _ ->
val grades = json.getJsonArray("Data")
@ -99,7 +124,7 @@ class VulcanApiGrades(override val data: DataVulcan,
category = category,
description = finalDescription,
comment = null,
semester = data.studentSemesterNumber,
semester = semesterNumber,
teacherId = teacherId,
subjectId = subjectId,
addedDate = addedDate
@ -115,9 +140,7 @@ class VulcanApiGrades(override val data: DataVulcan,
))
}
data.toRemove.add(DataRemoveModel.Grades.semesterWithType(data.studentSemesterNumber, Grade.TYPE_NORMAL))
data.setSyncNext(ENDPOINT_VULCAN_API_GRADES, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_API_GRADES)
onSuccess()
}
} ?: onSuccess(ENDPOINT_VULCAN_API_GRADES) }
}
}

View File

@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Notice
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.getLong
@ -30,6 +31,29 @@ class VulcanApiNotices(override val data: DataVulcan,
data.db.noticeTypeDao().getAllNow(profileId).toSparseArray(data.noticeTypes) { it.id }
}
val semesterId = data.studentSemesterId
val semesterNumber = data.studentSemesterNumber
if (semesterNumber == 2 && lastSync ?: 0 < profile.dateSemester1Start.inMillis) {
getNotices(profile, semesterId - 1, semesterNumber - 1) {
getNotices(profile, semesterId, semesterNumber) {
finish()
}
}
}
else {
getNotices(profile, semesterId, semesterNumber) {
finish()
}
}
} ?: onSuccess(ENDPOINT_VULCAN_API_NOTICES) }
private fun finish() {
data.setSyncNext(ENDPOINT_VULCAN_API_NOTICES, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_API_NOTICES)
}
private fun getNotices(profile: Profile, semesterId: Int, semesterNumber: Int, onSuccess: () -> Unit) {
apiGet(TAG, VULCAN_API_ENDPOINT_NOTICES, parameters = mapOf(
"IdUczen" to data.studentId,
"IdOkresKlasyfikacyjny" to data.studentSemesterId
@ -67,8 +91,7 @@ class VulcanApiNotices(override val data: DataVulcan,
))
}
data.setSyncNext(ENDPOINT_VULCAN_API_NOTICES, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_API_NOTICES)
onSuccess()
}
} ?: onSuccess(ENDPOINT_VULCAN_API_NOTICES) }
}
}

View File

@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER1_
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_FINAL
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_PROPOSED
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.getJsonObject
import pl.szczodrzynski.edziennik.utils.Utils
@ -27,32 +28,54 @@ class VulcanApiProposedGrades(override val data: DataVulcan,
init { data.profile?.also { profile ->
val semesterId = data.studentSemesterId
val semesterNumber = data.studentSemesterNumber
if (semesterNumber == 2 && lastSync ?: 0 < profile.dateSemester1Start.inMillis) {
getProposedGrades(profile, semesterId - 1, semesterNumber - 1) {
getProposedGrades(profile, semesterId, semesterNumber) {
finish()
}
}
}
else {
getProposedGrades(profile, semesterId, semesterNumber) {
finish()
}
}
} ?: onSuccess(ENDPOINT_VULCAN_API_GRADES_SUMMARY) }
private fun finish() {
data.setSyncNext(ENDPOINT_VULCAN_API_GRADES_SUMMARY, 6*HOUR)
onSuccess(ENDPOINT_VULCAN_API_GRADES_SUMMARY)
}
private fun getProposedGrades(profile: Profile, semesterId: Int, semesterNumber: Int, onSuccess: () -> Unit) {
apiGet(TAG, VULCAN_API_ENDPOINT_GRADES_PROPOSITIONS, parameters = mapOf(
"IdUczen" to data.studentId,
"IdOkresKlasyfikacyjny" to data.studentSemesterId
"IdOkresKlasyfikacyjny" to semesterId
)) { json, _ ->
val grades = json.getJsonObject("Data")
grades.getJsonArray("OcenyPrzewidywane")?.let {
processGradeList(it, isFinal = false)
processGradeList(it, semesterNumber, isFinal = false)
}
grades.getJsonArray("OcenyKlasyfikacyjne")?.let {
processGradeList(it, isFinal = true)
processGradeList(it, semesterNumber, isFinal = true)
}
data.setSyncNext(ENDPOINT_VULCAN_API_GRADES_SUMMARY, 6*HOUR)
onSuccess(ENDPOINT_VULCAN_API_GRADES_SUMMARY)
onSuccess()
}
} ?: onSuccess(ENDPOINT_VULCAN_API_GRADES_SUMMARY) }
}
private fun processGradeList(grades: JsonArray, isFinal: Boolean) {
grades.asJsonObjectList()?.forEach { grade ->
private fun processGradeList(grades: JsonArray, semesterNumber: Int, isFinal: Boolean) {
grades.asJsonObjectList().forEach { grade ->
val name = grade.get("Wpis").asString
val value = Utils.getGradeValue(name)
val subjectId = grade.get("IdPrzedmiot").asLong
val id = subjectId * -100 - data.studentSemesterNumber
val id = subjectId * -100 - semesterNumber
val color = Utils.getVulcanGradeColor(name)
@ -60,7 +83,7 @@ class VulcanApiProposedGrades(override val data: DataVulcan,
profileId = profileId,
id = id,
name = name,
type = if (data.studentSemesterNumber == 1) {
type = if (semesterNumber == 1) {
if (isFinal) TYPE_SEMESTER1_FINAL else TYPE_SEMESTER1_PROPOSED
} else {
if (isFinal) TYPE_SEMESTER2_FINAL else TYPE_SEMESTER2_PROPOSED
@ -71,7 +94,7 @@ class VulcanApiProposedGrades(override val data: DataVulcan,
category = "",
description = null,
comment = null,
semester = data.studentSemesterNumber,
semester = semesterNumber,
teacherId = -1,
subjectId = subjectId
)

View File

@ -80,7 +80,7 @@ class VulcanApiTimetable(override val data: DataVulcan,
id,
name,
Team.TYPE_VIRTUAL,
"${data.schoolName}:$name",
"${data.schoolCode}:$name",
teacherId ?: oldTeacherId ?: -1
)
data.teamList[id] = team

View File

@ -60,6 +60,7 @@ class VulcanApiUpdateSemester(override val data: DataVulcan,
data.studentClassId = studentClassId
data.studentSemesterId = studentSemesterId
data.studentSemesterNumber = studentSemesterNumber
data.profile.studentData["semester${studentSemesterNumber}Id"] = studentSemesterId
data.currentSemesterEndDate = currentSemesterEndDate
profile.studentClassName = studentClassName
dateSemester1Start?.let {

View File

@ -0,0 +1,11 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
enum class HebeFilterType(val endpoint: String) {
BY_PUPIL("byPupil"),
BY_PERSON("byPerson"),
BY_PERIOD("byPeriod")
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.core.util.set
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ADDRESSBOOK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ADDRESSBOOK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_EDUCATOR
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_OTHER
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENTS_COUNCIL
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_STUDENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER
import kotlin.text.replace
class VulcanHebeAddressbook(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeAddressbook"
}
private fun String.removeUnitName(unitName: String?): String {
return (unitName ?: data.schoolShort)?.let {
this.replace("($it)", "").trim()
} ?: this
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_ADDRESSBOOK,
HebeFilterType.BY_PERSON,
lastSync = lastSync,
includeFilterType = false
) { list, _ ->
list.forEach { person ->
val id = person.getString("Id") ?: return@forEach
val loginId = person.getString("LoginId") ?: return@forEach
val idType = id.split("-")
.getOrNull(0)
val idLong = id.split("-")
.getOrNull(1)
?.toLongOrNull()
?: return@forEach
val typeBase = when (idType) {
"e" -> TYPE_TEACHER
"c" -> TYPE_PARENT
"p" -> TYPE_STUDENT
else -> TYPE_OTHER
}
val name = person.getString("Name") ?: ""
val surname = person.getString("Surname") ?: ""
val namePrefix = "$surname $name - "
val teacher = data.teacherList[idLong] ?: Teacher(
data.profileId,
idLong,
name,
surname,
loginId
).also {
data.teacherList[idLong] = it
}
person.getJsonArray("Roles")?.asJsonObjectList()?.onEach { role ->
var roleText: String? = null
val unitName = role.getString("ConstituentUnitSymbol")
val personType = when (role.getInt("RoleOrder")) {
0 -> { /* Wychowawca */
roleText = role.getString("ClassSymbol")
?.removeUnitName(unitName)
TYPE_EDUCATOR
}
1 -> TYPE_TEACHER /* Nauczyciel */
2 -> return@onEach /* Pracownik */
3 -> { /* Rada rodziców */
roleText = role.getString("Address")
?.removeUnitName(unitName)
?.removePrefix(namePrefix)
?.trim()
TYPE_PARENTS_COUNCIL
}
5 -> {
roleText = role.getString("RoleName")
?.plus(" - ")
?.plus(
role.getString("Address")
?.removeUnitName(unitName)
?.removePrefix(namePrefix)
?.trim()
)
TYPE_STUDENT
}
else -> TYPE_OTHER
}
teacher.setTeacherType(personType)
teacher.typeDescription = roleText
}
if (teacher.type == 0)
teacher.setTeacherType(typeBase)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK)
}
}
}

View File

@ -0,0 +1,144 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2021-2-21
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ATTENDANCE
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ATTENDANCE
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class VulcanHebeAttendance(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeAttendance"
}
init {
val semesterNumber = data.studentSemesterNumber
val startDate = profile?.getSemesterStart(semesterNumber)
val endDate = profile?.getSemesterEnd(semesterNumber)
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_ATTENDANCE,
HebeFilterType.BY_PUPIL,
dateFrom = startDate,
dateTo = endDate,
lastSync = lastSync
) { list, _ ->
list.forEach { attendance ->
val id = attendance.getLong("AuxPresenceId") ?: return@forEach
val type = attendance.getJsonObject("PresenceType") ?: return@forEach
val baseType = getBaseType(type)
val typeName = type.getString("Name") ?: return@forEach
val typeCategoryId = type.getLong("CategoryId") ?: return@forEach
val typeSymbol = type.getString("Symbol") ?: return@forEach
val typeShort = when (typeCategoryId.toInt()) {
6, 8 -> typeSymbol
else -> data.app.attendanceManager.getTypeShort(baseType)
}
val typeColor = when (typeCategoryId.toInt()) {
1 -> 0xffffffff // obecność
2 -> 0xffffa687 // nieobecność
3 -> 0xfffcc150 // nieobecność usprawiedliwiona
4 -> 0xffede049 // spóźnienie
5 -> 0xffbbdd5f // spóźnienie usprawiedliwione
6 -> 0xffa9c9fd // nieobecny z przyczyn szkolnych
7 -> 0xffddbbe5 // zwolniony
8 -> 0xffffffff // usunięty wpis
else -> null
}?.toInt()
val date = getDate(attendance, "Day") ?: return@forEach
val lessonRange = getLessonRange(attendance, "TimeSlot")
val startTime = lessonRange?.startTime
val semester = profile?.dateToSemester(date) ?: return@forEach
val teacherId = attendance.getJsonObject("TeacherPrimary")?.getLong("Id") ?: -1
val subjectId = attendance.getJsonObject("Subject")?.getLong("Id") ?: -1
val addedDate = getDateTime(attendance, "DateModify")
val lessonNumber = lessonRange?.lessonNumber
val isCounted = attendance.getBoolean("CalculatePresence")
?: (baseType != Attendance.TYPE_RELEASED)
val attendanceObject = Attendance(
profileId = profileId,
id = id,
baseType = baseType,
typeName = typeName,
typeShort = typeShort,
typeSymbol = typeSymbol,
typeColor = typeColor,
date = date,
startTime = startTime,
semester = semester,
teacherId = teacherId,
subjectId = subjectId,
addedDate = addedDate
).also {
it.lessonTopic = attendance.getString("Topic")
it.lessonNumber = lessonNumber
it.isCounted = isCounted
}
data.attendanceList.add(attendanceObject)
if (baseType != Attendance.TYPE_PRESENT) {
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_ATTENDANCE,
attendanceObject.id,
profile?.empty ?: true
|| baseType == Attendance.TYPE_PRESENT_CUSTOM
|| baseType == Attendance.TYPE_UNKNOWN,
profile?.empty ?: true
|| baseType == Attendance.TYPE_PRESENT_CUSTOM
|| baseType == Attendance.TYPE_UNKNOWN
)
)
}
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ATTENDANCE, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_ATTENDANCE)
}
}
fun getBaseType(attendanceType: JsonObject): Int {
val absent = attendanceType.getBoolean("Absence") ?: false
val excused = attendanceType.getBoolean("AbsenceJustified") ?: false
return if (absent) {
if (excused)
Attendance.TYPE_ABSENT_EXCUSED
else
Attendance.TYPE_ABSENT
} else {
val belated = attendanceType.getBoolean("Late") ?: false
val released = attendanceType.getBoolean("LegalAbsence") ?: false
val present = attendanceType.getBoolean("Presence") ?: true
if (belated)
if (excused)
Attendance.TYPE_BELATED_EXCUSED
else
Attendance.TYPE_BELATED
else if (released)
Attendance.TYPE_RELEASED
else if (present)
if (attendanceType.getInt("CategoryId") != 1)
Attendance.TYPE_PRESENT_CUSTOM
else
Attendance.TYPE_PRESENT
else
Attendance.TYPE_UNKNOWN
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_EXAMS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_EXAMS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class VulcanHebeExams(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeExams"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_EXAMS,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { exam ->
val id = exam.getLong("Id") ?: return@forEach
val eventDate = getDate(exam, "Deadline") ?: return@forEach
val subjectId = getSubjectId(exam, "Subject") ?: -1
val teacherId = getTeacherId(exam, "Creator") ?: -1
val teamId = getTeamId(exam, "Distribution")
?: getClassId(exam, "Class")
?: data.teamClass?.id
?: -1
val topic = exam.getString("Content")?.trim() ?: ""
if (!isCurrentYear(eventDate)) return@forEach
val lessonList = data.db.timetableDao().getAllForDateNow(profileId, eventDate)
val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime
val type = when (exam.getString("Type")) {
"Praca klasowa",
"Sprawdzian" -> Event.TYPE_EXAM
"Kartkówka" -> Event.TYPE_SHORT_QUIZ
else -> Event.TYPE_DEFAULT
}
val eventObject = Event(
profileId = profileId,
id = id,
date = eventDate,
time = startTime,
topic = topic,
color = null,
type = type,
teacherId = teacherId,
subjectId = subjectId,
teamId = teamId
)
data.eventList.add(eventObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_EVENT,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_EXAMS, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_EXAMS)
}
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils
class VulcanHebeGradeSummary(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeGradeSummary"
}
init {
val entries = mapOf(
"Entry_1" to
if (data.studentSemesterNumber == 1)
Grade.TYPE_SEMESTER1_PROPOSED
else Grade.TYPE_SEMESTER2_PROPOSED,
"Entry_2" to
if (data.studentSemesterNumber == 1)
Grade.TYPE_SEMESTER1_FINAL
else Grade.TYPE_SEMESTER2_FINAL
)
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { grade ->
val subjectId = getSubjectId(grade, "Subject") ?: return@forEach
val addedDate = getDateTime(grade, "DateModify")
entries.onEach { (key, type) ->
val id = subjectId * -100 - type
val entry = grade.getString(key) ?: return@onEach
val value = Utils.getGradeValue(entry)
val color = Utils.getVulcanGradeColor(entry)
val gradeObject = Grade(
profileId = profileId,
id = id,
name = entry,
type = type,
value = value,
weight = 0f,
color = color,
category = "",
description = null,
comment = null,
semester = data.studentSemesterNumber,
teacherId = -1,
subjectId = subjectId,
addedDate = addedDate
)
data.gradeList.add(gradeObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_GRADE,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY, 1 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY)
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import java.text.DecimalFormat
import kotlin.math.roundToInt
class VulcanHebeGrades(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeGrades"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_GRADES,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { grade ->
val id = grade.getLong("Id") ?: return@forEach
val column = grade.getJsonObject("Column")
val category = column.getJsonObject("Category")
val categoryText = category.getString("Name")
val teacherId = getTeacherId(grade, "Creator") ?: -1
val subjectId = getSubjectId(column, "Subject") ?: -1
val description = column.getString("Name")
val comment = grade.getString("Comment")
var value = grade.getFloat("Value")
var weight = column.getFloat("Weight") ?: 0.0f
val numerator = grade.getFloat("Numerator ")
val denominator = grade.getFloat("Denominator")
val addedDate = getDateTime(grade, "DateModify")
var finalDescription = ""
var name = when (numerator != null && denominator != null) {
true -> {
value = numerator / denominator
finalDescription += DecimalFormat("#.##").format(numerator) +
"/" + DecimalFormat("#.##").format(denominator)
weight = 0.0f
(value * 100).roundToInt().toString() + "%"
}
else -> {
if (value == null) weight = 0.0f
grade.getString("Content") ?: ""
}
}
comment?.also {
if (name == "") name = it
else finalDescription = (if (finalDescription == "") "" else " ") + it
}
description?.also {
finalDescription = (if (finalDescription == "") "" else " - ") + it
}
val columnColor = column.getInt("Color") ?: 0
val color = if (columnColor == 0)
when (name) {
"1-", "1", "1+" -> 0xffd65757
"2-", "2", "2+" -> 0xff9071b3
"3-", "3", "3+" -> 0xffd2ab24
"4-", "4", "4+" -> 0xff50b6d6
"5-", "5", "5+" -> 0xff2cbd92
"6-", "6", "6+" -> 0xff91b43c
else -> 0xff3D5F9C
}.toInt()
else
columnColor
val gradeObject = Grade(
profileId = profileId,
id = id,
name = name,
type = Grade.TYPE_NORMAL,
value = value ?: 0.0f,
weight = weight,
color = color,
category = categoryText,
description = finalDescription,
comment = null,
semester = getSemester(column),
teacherId = teacherId,
subjectId = subjectId,
addedDate = addedDate
)
data.gradeList.add(gradeObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_GRADE,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_GRADES, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_GRADES)
}
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.asJsonObjectList
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_HOMEWORK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_HOMEWORK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils
class VulcanHebeHomework(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeHomework"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_HOMEWORK,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { exam ->
val id = exam.getLong("IdHomework") ?: return@forEach
val eventDate = getDate(exam, "Deadline") ?: return@forEach
val subjectId = getSubjectId(exam, "Subject") ?: -1
val teacherId = getTeacherId(exam, "Creator") ?: -1
val teamId = data.teamClass?.id ?: -1
val topic = exam.getString("Content")?.trim() ?: ""
if (!isCurrentYear(eventDate)) return@forEach
val lessonList = data.db.timetableDao().getAllForDateNow(profileId, eventDate)
val startTime = lessonList.firstOrNull { it.subjectId == subjectId }?.startTime
val eventObject = Event(
profileId = profileId,
id = id,
date = eventDate,
time = startTime,
topic = topic,
color = null,
type = Event.TYPE_HOMEWORK,
teacherId = teacherId,
subjectId = subjectId,
teamId = teamId
)
val attachments = exam.getJsonArray("Attachments")
?.asJsonObjectList()
?: return@forEach
for (attachment in attachments) {
val fileName = attachment.getString("Name") ?: continue
val url = attachment.getString("Link") ?: continue
val attachmentName = "$fileName:$url"
val attachmentId = Utils.crc32(attachmentName.toByteArray())
eventObject.addAttachment(
id = attachmentId,
name = attachmentName
)
}
data.eventList.add(eventObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_HOMEWORK,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_HOMEWORK, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_HOMEWORK)
}
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class VulcanHebeLuckyNumber(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeLuckyNumber"
}
init {
apiGet(
TAG,
VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER,
query = mapOf(
"constituentId" to data.studentConstituentId.toString(),
"day" to Date.getToday().stringY_m_d
)
) { lucky: JsonObject?, _ ->
// sync tomorrow if lucky number set or is weekend or afternoon
var nextSync = Date.getToday().stepForward(0, 0, 1).inMillis
if (lucky == null) {
if (Date.getToday().weekDay <= Week.FRIDAY && Time.getNow().hour < 12) {
// working days morning, sync always
nextSync = SYNC_ALWAYS
}
}
else {
val luckyNumberDate = Date.fromY_m_d(lucky.getString("Day")) ?: Date.getToday()
val luckyNumber = lucky.getInt("Number") ?: -1
val luckyNumberObject = LuckyNumber(
profileId = profileId,
date = luckyNumberDate,
number = luckyNumber
)
data.luckyNumberList.add(luckyNumberObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_LUCKY_NUMBER,
luckyNumberObject.date.value.toLong(),
true,
profile?.empty ?: false
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER, syncAt = nextSync)
onSuccess(ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER)
}
}
}

View File

@ -0,0 +1,164 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonArray
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.models.Date
class VulcanHebeMain(
override val data: DataVulcan,
override val lastSync: Long? = null
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeMain"
}
fun getStudents(
profile: Profile?,
profileList: MutableList<Profile>?,
loginStoreId: Int? = null,
firstProfileId: Int? = null,
onEmpty: (() -> Unit)? = null,
onSuccess: () -> Unit
) {
if (profile == null && (profileList == null || loginStoreId == null || firstProfileId == null))
throw IllegalArgumentException()
apiGet(
TAG,
VULCAN_HEBE_ENDPOINT_MAIN,
query = mapOf("lastSyncDate" to "null"),
baseUrl = profile == null
) { students: JsonArray, _ ->
if (students.isEmpty()) {
if (onEmpty != null)
onEmpty()
else
onSuccess()
return@apiGet
}
// safe to assume this will be non-null when creating a profile
var profileId = firstProfileId ?: loginStoreId ?: 1
students.forEach { studentEl ->
val student = studentEl.asJsonObject
val pupil = student.getJsonObject("Pupil")
val studentId = pupil.getInt("Id") ?: return@forEach
// check the student ID in case of not first login
if (profile != null && data.studentId != studentId)
return@forEach
val unit = student.getJsonObject("Unit")
val constituentUnit = student.getJsonObject("ConstituentUnit")
val login = student.getJsonObject("Login")
val periods = student.getJsonArray("Periods")?.map {
it.asJsonObject
} ?: listOf()
val period = periods.firstOrNull {
it.getBoolean("Current", false)
} ?: return@forEach
val periodLevel = period.getInt("Level") ?: return@forEach
val semester1 = periods.firstOrNull {
it.getInt("Level") == periodLevel && it.getInt("Number") == 1
}
val semester2 = periods.firstOrNull {
it.getInt("Level") == periodLevel && it.getInt("Number") == 2
}
val schoolSymbol = unit.getString("Symbol") ?: return@forEach
val schoolShort = constituentUnit.getString("Short") ?: return@forEach
val schoolCode = "${data.symbol}_$schoolSymbol"
val studentUnitId = unit.getInt("Id") ?: return@forEach
val studentConstituentId = constituentUnit.getInt("Id") ?: return@forEach
val studentLoginId = login.getInt("Id") ?: return@forEach
//val studentClassId = student.getInt("IdOddzial") ?: return@forEach
val studentClassName = student.getString("ClassDisplay") ?: return@forEach
val studentFirstName = pupil.getString("FirstName") ?: ""
val studentLastName = pupil.getString("Surname") ?: ""
val studentNameLong = "$studentFirstName $studentLastName".fixName()
val studentNameShort = "$studentFirstName ${studentLastName[0]}.".fixName()
val userLogin = login.getString("Value") ?: ""
val studentSemesterId = period.getInt("Id") ?: return@forEach
val studentSemesterNumber = period.getInt("Number") ?: return@forEach
val hebeContext = student.getString("Context")
val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true)
val accountName = if (isParent)
login.getString("DisplayName")?.fixName()
else null
val dateSemester1Start = semester1
?.getJsonObject("Start")
?.getString("Date")
?.let { Date.fromY_m_d(it) }
val dateSemester2Start = semester2
?.getJsonObject("Start")
?.getString("Date")
?.let { Date.fromY_m_d(it) }
val dateYearEnd = semester2
?.getJsonObject("End")
?.getString("Date")
?.let { Date.fromY_m_d(it) }
val newProfile = profile ?: Profile(
profileId++,
loginStoreId!!,
LOGIN_TYPE_VULCAN,
studentNameLong,
userLogin,
studentNameLong,
studentNameShort,
accountName
)
newProfile.apply {
this.studentClassName = studentClassName
studentData["symbol"] = data.symbol
studentData["studentId"] = studentId
studentData["studentUnitId"] = studentUnitId
studentData["studentConstituentId"] = studentConstituentId
studentData["studentLoginId"] = studentLoginId
studentData["studentSemesterId"] = studentSemesterId
studentData["studentSemesterNumber"] = studentSemesterNumber
studentData["semester1Id"] = semester1?.getInt("Id") ?: 0
studentData["semester2Id"] = semester2?.getInt("Id") ?: 0
studentData["schoolSymbol"] = schoolSymbol
studentData["schoolShort"] = schoolShort
studentData["schoolName"] = schoolCode
studentData["hebeContext"] = hebeContext
}
dateSemester1Start?.let {
newProfile.dateSemester1Start = it
newProfile.studentSchoolYearStart = it.year
}
dateSemester2Start?.let { newProfile.dateSemester2Start = it }
dateYearEnd?.let { newProfile.dateYearEnd = it }
if (profile != null)
data.setSyncNext(ENDPOINT_VULCAN_HEBE_MAIN, 1 * DAY)
profileList?.add(newProfile)
}
onSuccess()
}
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.core.util.set
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.navlib.crc16
import kotlin.text.replace
class VulcanHebeMessages(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeMessagesInbox"
}
private fun getPersonId(json: JsonObject): Long {
val senderLoginId = json.getInt("LoginId") ?: return -1
/*if (senderLoginId == data.studentLoginId)
return -1*/
val senderName = json.getString("Address") ?: return -1
val senderNameSplit = senderName.splitName()
val senderLoginIdStr = senderLoginId.toString()
val teacher = data.teacherList.singleOrNull { it.loginId == senderLoginIdStr }
?: Teacher(
profileId,
-1 * crc16(senderName).toLong(),
senderNameSplit?.second ?: "",
senderNameSplit?.first ?: "",
senderLoginIdStr
).also {
it.setTeacherType(Teacher.TYPE_OTHER)
data.teacherList[it.id] = it
}
return teacher.id
}
fun getMessages(messageType: Int) {
val folder = when (messageType) {
TYPE_RECEIVED -> 1
TYPE_SENT -> 2
TYPE_DELETED -> 3
else -> 1
}
val endpointId = when (messageType) {
TYPE_RECEIVED -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
TYPE_SENT -> ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
else -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
}
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES,
HebeFilterType.BY_PERSON,
folder = folder,
lastSync = lastSync
) { list, _ ->
list.forEach { message ->
val id = message.getLong("Id") ?: return@forEach
val subject = message.getString("Subject") ?: return@forEach
val body = message.getString("Content") ?: return@forEach
val sender = message.getJsonObject("Sender") ?: return@forEach
val sentDate = getDateTime(message, "DateSent")
val readDate = getDateTime(message, "DateRead", default = 0)
if (!isCurrentYear(sentDate)) return@forEach
val messageObject = Message(
profileId = profileId,
id = id,
type = messageType,
subject = subject,
body = body.replace("\n", "<br>"),
senderId = if (messageType == TYPE_RECEIVED) getPersonId(sender) else null,
addedDate = sentDate
)
val receivers = message.getJsonArray("Receiver")
?.asJsonObjectList()
?: return@forEach
val receiverReadDate =
if (receivers.size == 1) readDate
else -1
for (receiver in receivers) {
val messageRecipientObject = MessageRecipient(
profileId,
if (messageType == TYPE_SENT) getPersonId(receiver) else -1,
-1,
receiverReadDate,
id
)
data.messageRecipientList.add(messageRecipientObject)
}
val attachments = message.getJsonArray("Attachments")
?.asJsonObjectList()
?: return@forEach
for (attachment in attachments) {
val fileName = attachment.getString("Name") ?: continue
val url = attachment.getString("Link") ?: continue
val attachmentName = "$fileName:$url"
val attachmentId = Utils.crc32(attachmentName.toByteArray())
messageObject.addAttachment(
id = attachmentId,
name = attachmentName,
size = -1
)
}
data.messageList.add(messageObject)
data.setSeenMetadataList.add(
Metadata(
profileId,
Metadata.TYPE_MESSAGE,
id,
readDate > 0 || messageType == TYPE_SENT,
readDate > 0 || messageType == TYPE_SENT
)
)
}
data.setSyncNext(
endpointId,
if (messageType == TYPE_RECEIVED) SYNC_ALWAYS else 1 * DAY,
if (messageType == TYPE_RECEIVED) null else DRAWER_ITEM_MESSAGES
)
onSuccess(endpointId)
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.JsonObject
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
class VulcanHebeMessagesChangeStatus(
override val data: DataVulcan,
private val messageObject: MessageFull,
val onSuccess: () -> Unit
) : VulcanHebe(data, null) {
companion object {
const val TAG = "VulcanHebeMessagesChangeStatus"
}
init {
apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS,
payload = JsonObject(
"MessageId" to messageObject.id,
"LoginId" to data.studentLoginId,
"Status" to 1
)
) { _: Boolean, _ ->
if (!messageObject.seen) {
data.setSeenMetadataList.add(
Metadata(
profileId,
Metadata.TYPE_MESSAGE,
messageObject.id,
true,
true
)
)
messageObject.seen = true
}
if (messageObject.type != Message.TYPE_SENT) {
val messageRecipientObject = MessageRecipient(
profileId,
-1,
-1,
System.currentTimeMillis(),
messageObject.id
)
data.messageRecipientList.add(messageRecipientObject)
}
EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess()
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2021-2-22
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_NOTICES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_NOTICES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Notice
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class VulcanHebeNotices(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeNotices"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_NOTICES,
HebeFilterType.BY_PUPIL,
lastSync = lastSync
) { list, _ ->
list.forEach { notice ->
val id = notice.getLong("Id") ?: return@forEach
val type = when (notice.getBoolean("Positive")) {
true -> Notice.TYPE_POSITIVE
else -> Notice.TYPE_NEUTRAL
}
val date = getDate(notice, "DateValid") ?: return@forEach
val semester = profile?.dateToSemester(date) ?: return@forEach
val text = notice.getString("Content") ?: ""
val category = notice.getJsonObject("Category")?.getString("Name")
val points = notice.getFloat("Points")
val teacherId = getTeacherId(notice, "Creator") ?: -1
val addedDate = getDateTime(notice, "DateModify")
if (!isCurrentYear(date)) return@forEach
val noticeObject = Notice(
profileId = profileId,
id = id,
type = type,
semester = semester,
text = text,
category = category,
points = points,
teacherId = teacherId,
addedDate = addedDate
)
data.noticeList.add(noticeObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_NOTICE,
id,
profile?.empty ?: true,
profile?.empty ?: true
)
)
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_NOTICES, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_NOTICES)
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonPrimitive
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_PUSH_ALL
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_PUSH_CONFIG
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class VulcanHebePushConfig(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebePushConfig"
}
init {
apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_PUSH_ALL,
payload = JsonPrimitive("on")
) { _: Boolean, _ ->
// sync always: this endpoint has .shouldSync set
data.setSyncNext(ENDPOINT_VULCAN_HEBE_PUSH_CONFIG, SYNC_ALWAYS)
data.app.config.sync.tokenVulcanList =
data.app.config.sync.tokenVulcanList + profileId
onSuccess(ENDPOINT_VULCAN_HEBE_PUSH_CONFIG)
}
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-22.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
class VulcanHebeSendMessage(
override val data: DataVulcan,
val recipients: List<Teacher>,
val subject: String,
val text: String,
val onSuccess: () -> Unit
) : VulcanHebe(data, null) {
companion object {
const val TAG = "VulcanHebeSendMessage"
}
init {
val recipientsArray = JsonArray()
recipients.forEach { teacher ->
recipientsArray += JsonObject(
"Address" to teacher.fullNameLastFirst,
"LoginId" to (teacher.loginId?.toIntOrNull() ?: return@forEach),
"Initials" to teacher.initialsLastFirst,
"AddressHash" to teacher.fullNameLastFirst.sha1Hex()
)
}
val senderName = (profile?.accountName ?: profile?.studentNameLong)
?.swapFirstLastName() ?: ""
val sender = JsonObject(
"Address" to senderName,
"LoginId" to data.studentLoginId.toString(),
"Initials" to senderName.getNameInitials(),
"AddressHash" to senderName.sha1Hex()
)
apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES_SEND,
payload = JsonObject(
"Status" to 1,
"Sender" to sender,
"DateSent" to null,
"DateRead" to null,
"Content" to text,
"Receiver" to recipientsArray,
"Id" to 0,
"Subject" to subject,
"Attachments" to null,
"Self" to null
)
) { json: JsonObject, _ ->
val messageId = json.getLong("Id")
if (messageId == null) {
// TODO error
return@apiPost
}
VulcanHebeMessages(data, null) {
val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject }
val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId }
val event = MessageSentEvent(data.profileId, message, message?.addedDate)
EventBus.getDefault().postSticky(event)
onSuccess()
}.getMessages(Message.TYPE_SENT)
}
}
}

View File

@ -0,0 +1,251 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NORMAL
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_SHIFTED_SOURCE
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_SHIFTED_TARGET
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
class VulcanHebeTimetable(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeTimetable"
}
private val lessonList = mutableListOf<Lesson>()
private val lessonDates = mutableSetOf<Int>()
init {
val previousWeekStart = Week.getWeekStart().stepForward(0, 0, -7)
if (Date.getToday().weekDay > 4) {
previousWeekStart.stepForward(0, 0, 7)
}
val dateFrom = data.arguments
?.getString("weekStart")
?.let { Date.fromY_m_d(it) }
?: previousWeekStart
val dateTo = dateFrom.clone().stepForward(0, 0, 13)
val lastSync = null
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_TIMETABLE,
HebeFilterType.BY_PUPIL,
dateFrom = dateFrom,
dateTo = dateTo,
lastSync = lastSync
) { lessons, _ ->
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES,
HebeFilterType.BY_PUPIL,
dateFrom = dateFrom,
dateTo = dateTo,
lastSync = lastSync
) { changes, _ ->
processData(lessons, changes)
// cancel lesson changes when caused by a shift
for (lesson in lessonList) {
if (lesson.type != TYPE_SHIFTED_TARGET)
continue
lessonList.firstOrNull {
it.oldDate == lesson.date
&& it.oldLessonNumber == lesson.lessonNumber
&& it.type == TYPE_CHANGE
}?.let {
it.type = TYPE_CANCELLED
it.date = null
it.lessonNumber = null
it.startTime = null
it.endTime = null
it.subjectId = null
it.teacherId = null
it.teamId = null
it.classroom = null
}
}
// add TYPE_NO_LESSONS to empty dates
val date: Date = dateFrom.clone()
while (date <= dateTo) {
if (!lessonDates.contains(date.value)) {
lessonList.add(Lesson(profileId, date.value.toLong()).apply {
this.type = Lesson.TYPE_NO_LESSONS
this.date = date.clone()
})
}
date.stepForward(0, 0, 1)
}
d(
TAG,
"Clearing lessons between ${dateFrom.stringY_m_d} and ${dateTo.stringY_m_d}"
)
data.lessonList.addAll(lessonList)
data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_HEBE_TIMETABLE)
}
}
}
private fun buildLesson(changes: List<JsonObject>, json: JsonObject): Pair<Lesson, Lesson?>? {
val lesson = Lesson(profileId, -1)
var lessonShift: Lesson? = null
val lessonDate = getDate(json, "Date") ?: return null
val lessonRange = getLessonRange(json, "TimeSlot")
val startTime = lessonRange?.startTime
val endTime = lessonRange?.endTime
val teacherId = getTeacherId(json, "TeacherPrimary")
val classroom = json.getJsonObject("Room").getString("Code")
val subjectId = getSubjectId(json, "Subject")
val teamId = getTeamId(json, "Distribution")
?: getClassId(json, "Clazz")
?: data.teamClass?.id
?: -1
val change = json.getJsonObject("Change")
val changeId = change.getInt("Id")
val type = when (change.getInt("Type")) {
1 -> TYPE_CANCELLED
2 -> TYPE_CHANGE
3 -> TYPE_SHIFTED_SOURCE
4 -> TYPE_CANCELLED // TODO: 2021-02-21 add showing cancellation reason
else -> TYPE_NORMAL
}
lesson.type = type
if (type == TYPE_NORMAL) {
lesson.date = lessonDate
lesson.lessonNumber = lessonRange?.lessonNumber
lesson.startTime = startTime
lesson.endTime = endTime
lesson.subjectId = subjectId
lesson.teacherId = teacherId
lesson.teamId = teamId
lesson.classroom = classroom
} else {
lesson.oldDate = lessonDate
lesson.oldLessonNumber = lessonRange?.lessonNumber
lesson.oldStartTime = startTime
lesson.oldEndTime = endTime
lesson.oldSubjectId = subjectId
lesson.oldTeacherId = teacherId
lesson.oldTeamId = teamId
lesson.oldClassroom = classroom
}
if (type == TYPE_CHANGE || type == TYPE_SHIFTED_SOURCE) {
val changeJson = changes.firstOrNull {
it.getInt("Id") == changeId
} ?: return lesson to null
val changeLessonDate = getDate(changeJson, "LessonDate") ?: return lesson to null
val changeLessonRange = getLessonRange(changeJson, "TimeSlot") ?: lessonRange
val changeStartTime = changeLessonRange?.startTime
val changeEndTime = changeLessonRange?.endTime
val changeTeacherId = getTeacherId(changeJson, "TeacherPrimary") ?: teacherId
val changeClassroom = changeJson.getJsonObject("Room").getString("Code") ?: classroom
val changeSubjectId = getSubjectId(changeJson, "Subject") ?: subjectId
val changeTeamId = getTeamId(json, "Distribution")
?: getClassId(json, "Clazz")
?: teamId
if (type != TYPE_CHANGE) {
/* lesson shifted */
lessonShift = Lesson(profileId, -1)
lessonShift.type = TYPE_SHIFTED_TARGET
// update source lesson with the target lesson date
lesson.date = changeLessonDate
lesson.lessonNumber = changeLessonRange?.lessonNumber
lesson.startTime = changeStartTime
lesson.endTime = changeEndTime
// update target lesson with the source lesson date
lessonShift.oldDate = lessonDate
lessonShift.oldLessonNumber = lessonRange?.lessonNumber
lessonShift.oldStartTime = startTime
lessonShift.oldEndTime = endTime
}
(if (type == TYPE_CHANGE) lesson else lessonShift)
?.apply {
this.date = changeLessonDate
this.lessonNumber = changeLessonRange?.lessonNumber
this.startTime = changeStartTime
this.endTime = changeEndTime
this.subjectId = changeSubjectId
this.teacherId = changeTeacherId
this.teamId = changeTeamId
this.classroom = changeClassroom
}
}
return lesson to lessonShift
}
private fun processData(lessons: List<JsonObject>, changes: List<JsonObject>) {
lessons.forEach { lessonJson ->
if (lessonJson.getBoolean("Visible") != true)
return@forEach
val lessonPair = buildLesson(changes, lessonJson) ?: return@forEach
val (lessonObject, lessonShift) = lessonPair
when {
lessonShift != null -> lessonShift
lessonObject.type != TYPE_NORMAL -> lessonObject
else -> null
}?.let { lesson ->
val lessonDate = lesson.displayDate ?: return@let
val seen = profile?.empty ?: true || lessonDate < Date.getToday()
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
lesson.id,
seen,
seen
)
)
}
lessonObject.id = lessonObject.buildId()
lessonShift?.id = lessonShift?.buildId() ?: -1
lessonList.add(lessonObject)
lessonShift?.let { lessonList.add(it) }
lessonObject.displayDate?.let { lessonDates.add(it.value) }
lessonShift?.displayDate?.let { lessonDates.add(it.value) }
}
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web
import com.google.gson.annotations.SerializedName
data class HomepageTile(
@SerializedName("Nazwa")
val name: String?,
@SerializedName("Url")
val url: String?,
@SerializedName("Zawartosc")
val children: List<HomepageTile>
)

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-20.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.data.api.VULCAN_WEB_ENDPOINT_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain
import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class VulcanWebLuckyNumber(override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanWebMain(data, lastSync) {
companion object {
const val TAG = "VulcanWebLuckyNumber"
}
init {
webGetJson(TAG, WEB_MAIN, VULCAN_WEB_ENDPOINT_LUCKY_NUMBER, parameters = mapOf(
"permissions" to data.webPermissions
)) { json, _ ->
val tiles = json
.getJsonArray("data")
?.mapNotNull { data.app.gson.fromJson(it.toString(), HomepageTile::class.java) }
?.flatMap { it.children }
if (tiles == null) {
data.setSyncNext(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS)
return@webGetJson
}
var nextSync = System.currentTimeMillis() + 1* DAY *1000
tiles.firstOrNull { it.name == data.schoolShort }?.children?.firstOrNull()?.let { tile ->
// "Szczęśliwy numer w dzienniku: 16"
return@let tile.name?.substringAfterLast(' ')?.toIntOrNull()?.let { number ->
// lucky number present
val luckyNumberObject = LuckyNumber(
profileId,
Date.getToday(),
number
)
data.luckyNumberList.add(luckyNumberObject)
data.metadataList.add(
Metadata(
profileId,
Metadata.TYPE_LUCKY_NUMBER,
luckyNumberObject.date.value.toLong(),
true,
profile?.empty ?: false
))
}
} ?: {
// no lucky number
if (Date.getToday().weekDay <= Week.FRIDAY && Time.getNow().hour >= 22) {
// working days, after 10PM
// consider the lucky number is disabled; sync in 4 days
nextSync = System.currentTimeMillis() + 4*DAY*1000
}
else if (Date.getToday().weekDay <= Week.FRIDAY && Time.getNow().hour < 22) {
// working days, before 10PM
}
else {
// weekends
nextSync = Week.getNearestWeekDayDate(Week.MONDAY).combineWith(Time(5, 0, 0))
}
}()
data.setSyncNext(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS, SYNC_ALWAYS)
onSuccess(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS)
}
}
}

View File

@ -6,12 +6,18 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN
import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_STUDENT_LIST
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeMain
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.models.Date
@ -21,19 +27,104 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
}
private val api = VulcanApi(data, null)
private val web = VulcanWebMain(data, null)
private val hebe = VulcanHebe(data, null)
private val profileList = mutableListOf<Profile>()
private val loginStoreId = data.loginStore.id
private var firstProfileId = loginStoreId
private val tryingSymbols = mutableListOf<String>()
init {
val loginStoreId = data.loginStore.id
val loginStoreType = LOGIN_TYPE_VULCAN
var firstProfileId = loginStoreId
if (data.loginStore.mode == LOGIN_MODE_VULCAN_WEB) {
VulcanLoginWebMain(data) {
val xml = web.readCertificate() ?: run {
data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_CERTIFICATE))
return@VulcanLoginWebMain
}
val certificate = web.parseCertificate(xml)
if (data.symbol != null && data.symbol != "default") {
tryingSymbols += data.symbol ?: "default"
}
else {
tryingSymbols += certificate.userInstances
}
checkSymbol(certificate)
}
}
else if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) {
registerDevice {
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}
else {
registerDeviceHebe {
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}
}
private fun checkSymbol(certificate: CufsCertificate) {
if (tryingSymbols.isEmpty()) {
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
return
}
val result = web.postCertificate(certificate, tryingSymbols.removeAt(0)) { symbol, state ->
when (state) {
VulcanWebMain.STATE_NO_REGISTER -> {
checkSymbol(certificate)
}
VulcanWebMain.STATE_LOGGED_OUT -> data.error(ApiError(TAG, ERROR_VULCAN_WEB_LOGGED_OUT))
VulcanWebMain.STATE_SUCCESS -> {
webRegisterDevice(symbol) {
checkSymbol(certificate)
}
}
}
}
// postCertificate returns false if the cert is not valid anymore
if (!result) {
data.error(ApiError(TAG, ERROR_VULCAN_WEB_CERTIFICATE_EXPIRED)
.withApiResponse(certificate.xml))
}
}
private fun webRegisterDevice(symbol: String, onSuccess: () -> Unit) {
web.getStartPage(symbol, postErrors = false) { _, schoolSymbols ->
if (schoolSymbols.isEmpty()) {
onSuccess()
return@getStartPage
}
data.symbol = symbol
val schoolSymbol = data.schoolSymbol ?: schoolSymbols.firstOrNull()
web.webGetJson(TAG, VulcanWebMain.WEB_NEW, "$schoolSymbol/$VULCAN_WEB_ENDPOINT_REGISTER_DEVICE") { result, _ ->
val json = result.getJsonObject("data")
data.symbol = symbol
data.apiToken = data.apiToken.toMutableMap().also {
it[symbol] = json.getString("TokenKey")
}
data.apiPin = data.apiPin.toMutableMap().also {
it[symbol] = json.getString("PIN")
}
registerDeviceHebe(onSuccess)
}
}
}
private fun registerDevice(onSuccess: () -> Unit) {
VulcanLoginApi(data) {
api.apiGet(TAG, VULCAN_API_ENDPOINT_STUDENT_LIST, baseUrl = true) { json, response ->
api.apiGet(TAG, VULCAN_API_ENDPOINT_STUDENT_LIST, baseUrl = true) { json, _ ->
val students = json.getJsonArray("Data")
if (students == null || students.isEmpty()) {
EventBus.getDefault().post(FirstLoginFinishedEvent(listOf(), data.loginStore))
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore))
onSuccess()
return@apiGet
}
@ -42,7 +133,8 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
val student = studentEl.asJsonObject
val schoolSymbol = student.getString("JednostkaSprawozdawczaSymbol") ?: return@forEach
val schoolName = "${data.symbol}_$schoolSymbol"
val schoolShort = student.getString("JednostkaSprawozdawczaSkrot") ?: return@forEach
val schoolCode = "${data.symbol}_$schoolSymbol"
val studentId = student.getInt("Id") ?: return@forEach
val studentLoginId = student.getInt("UzytkownikLoginId") ?: return@forEach
val studentClassId = student.getInt("IdOddzial") ?: return@forEach
@ -80,7 +172,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
val profile = Profile(
firstProfileId++,
loginStoreId,
loginStoreType,
LOGIN_TYPE_VULCAN,
studentNameLong,
userLogin,
studentNameLong,
@ -88,13 +180,17 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
accountName
).apply {
this.studentClassName = studentClassName
studentData["symbol"] = data.symbol
studentData["studentId"] = studentId
studentData["studentLoginId"] = studentLoginId
studentData["studentClassId"] = studentClassId
studentData["studentSemesterId"] = studentSemesterId
studentData["studentSemesterNumber"] = studentSemesterNumber
studentData["semester${studentSemesterNumber}Id"] = studentSemesterId
studentData["schoolSymbol"] = schoolSymbol
studentData["schoolName"] = schoolName
studentData["schoolShort"] = schoolShort
studentData["schoolName"] = schoolCode
studentData["currentSemesterEndDate"] = currentSemesterEndDate
}
dateSemester1Start?.let {
@ -107,9 +203,25 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
profileList.add(profile)
}
EventBus.getDefault().post(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}
}
private fun registerDeviceHebe(onSuccess: () -> Unit) {
VulcanLoginHebe(data) {
VulcanHebeMain(data).getStudents(
profile = null,
profileList,
loginStoreId,
firstProfileId,
onEmpty = {
EventBus.getDefault()
.postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore))
onSuccess()
},
onSuccess = onSuccess
)
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-4-17.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login
import pl.droidsonroids.jspoon.annotation.Selector
class CufsCertificate {
@Selector(value = "EndpointReference Address")
var targetUrl: String = ""
@Selector(value = "Lifetime Created")
var createdDate: String = ""
@Selector(value = "Lifetime Expires")
var expiryDate: String = ""
@Selector(value = "Attribute[AttributeName=UserInstance] AttributeValue")
var userInstances: List<String> = listOf()
var xml = ""
}

View File

@ -6,6 +6,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.utils.Utils
@ -45,10 +47,18 @@ class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
}
Utils.d(TAG, "Using login method $loginMethodId")
when (loginMethodId) {
LOGIN_METHOD_VULCAN_WEB_MAIN -> {
data.startProgress(R.string.edziennik_progress_login_vulcan_web_main)
VulcanLoginWebMain(data) { onSuccess(loginMethodId) }
}
LOGIN_METHOD_VULCAN_API -> {
data.startProgress(R.string.edziennik_progress_login_vulcan_api)
VulcanLoginApi(data) { onSuccess(loginMethodId) }
}
LOGIN_METHOD_VULCAN_HEBE -> {
data.startProgress(R.string.edziennik_progress_login_vulcan_api)
VulcanLoginHebe(data) { onSuccess(loginMethodId) }
}
}
}
}

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