Compare commits

...

45 Commits

Author SHA1 Message Date
6f12227c2e [4.13-beta.1] Update build.gradle, signing and changelog. 2022-10-17 22:56:22 +02:00
3a91f87ccd Merge pull request #142 from szkolny-eu/feature/usos
[API] Add USOS API support.
2022-10-17 22:52:08 +02:00
52a53334ca [Strings] Add USOS error descriptions. 2022-10-17 22:30:23 +02:00
3ab9602865 [API/Usos] Fix re-logging in after user action. 2022-10-17 16:06:29 +02:00
dc19043f73 [API/Usos] Implement basic error handling. 2022-10-17 12:56:07 +02:00
cf25507850 [API/Usos] Save lecturers as teachers. Add team class. 2022-10-16 18:41:37 +02:00
044cedff99 [Usos] Override lesson colors by activity type. 2022-10-16 18:13:22 +02:00
4de066bf5f [API/Usos] Implement Timetable. 2022-10-16 17:21:36 +02:00
8d174bda01 Merge remote-tracking branch 'origin/develop' into feature/usos 2022-10-16 16:54:44 +02:00
e2fd714070 [UI/Timetable] Show subject-based lesson colors. (#141)
* [UI/Timetable] Add timetable config dialog and implement UI options.

* [UI/Timetable] Fix reloading timetable after changing config.

* [UI/Timetable] Fix calculating lesson range boundaries.

* [UI/Timetable] Add coloring subject names.
2022-10-16 12:47:54 +02:00
8097e8d06d [API/Usos] Add syncing Courses and Terms. 2022-10-16 00:09:51 +02:00
93ccdbdeb7 [API/Login] Make user action handling more universal. 2022-10-15 21:24:30 +02:00
7ded400a30 [API/Usos] Implement first login. 2022-10-15 19:07:12 +02:00
2ff784066e [API/Usos] Implement OAuth authorization flow. 2022-10-14 21:44:58 +02:00
6c96875c83 [API/Login] Allow passing LoginStore params in user action requests. 2022-10-14 19:44:22 +02:00
9f3aaf6e86 [API] Move register platforms to new endpoint. 2022-10-14 14:43:40 +02:00
55369eaa8b [UI/Timetable] Add Timetable settings. (#140)
* [UI/Timetable] Add timetable config dialog and implement UI options.

* [UI/Timetable] Fix reloading timetable after changing config.

* [UI/Timetable] Fix calculating lesson range boundaries.
2022-10-14 00:25:22 +02:00
c983c16907 [UI] Request notifications permission on API >= 33. (#143)
* Added requesting notifications permission on Android 13 devices

* Move permission check to PermissionManager

* Request permission on home screen
2022-10-14 00:24:00 +02:00
c7362bce12 [API/Usos] Add base rest API class. 2022-10-13 21:27:55 +02:00
7935d0f097 [API] Pass parameters to user action required errors. 2022-10-13 11:45:17 +02:00
4b64277948 [API/Usos] Add basic USOS API structure. 2022-10-11 23:23:11 +02:00
132729bbd9 [4.12.1] Update build.gradle, signing and changelog. 2022-09-23 11:32:08 +02:00
6d36ab27d1 [UI/Login] Fix QR scanning icon not showing. 2022-09-23 11:30:23 +02:00
11fabb231f [4.12] Update build.gradle, signing and changelog. 2022-09-21 21:09:23 +02:00
6fd999f88c [UI/Widget] Fix showing lesson details dialog. 2022-09-21 21:02:09 +02:00
7711413b30 [API/Vulcan] Fix getting MessageBoxes and changing message read status. 2022-09-21 20:43:18 +02:00
cdc0c9d458 [4.11.9] Update build.gradle, signing and changelog. 2022-09-18 23:06:41 +02:00
8e5f750a80 [Manifest] Remove APK install to comply with Google Play policies. 2022-09-18 23:05:46 +02:00
f4e7e8978c [API/Vulcan] Ensure messageBoxKey before syncing addressbook. 2022-09-18 22:52:43 +02:00
96c542d6d2 [4.11.8] Update build.gradle, signing and changelog. 2022-09-17 18:02:11 +02:00
77d22b87aa [API] Move unused resources to the e-register graveyard. 2022-09-17 17:49:56 +02:00
37c68443bd [API] Remove EduDziennik implementation [*] 2022-09-17 17:39:45 +02:00
9dbb5d70e9 [App] Fix PendingIntent crashing on API 31+. 2022-09-17 17:21:13 +02:00
99afa77a63 [4.11.7] Update build.gradle, signing and changelog. 2022-09-17 16:00:38 +02:00
a5d0f4212d [UI/Login] Make Lab accessible from login page. 2022-09-17 15:00:34 +02:00
a85f935eb4 [UI/Home] Improve displaying cancelled lessons in timetable card. 2022-09-17 14:58:54 +02:00
bb44fa066c [API/Vulcan] Migrate to new MessageBox API. (#134)
* Implement fetching vulcan messages from new api

* Bump kotlin version and fix timetable card lessons size display

* Revert formatting changes

* Revert disabling message composing

* Revert MultiDex changes and dependency upgrades

* Add missing MultiDex dependency, update Google Services

* Separate MessageBoxes sync, revert API data behavior changes

* Revert migrating MessageRecipient to Kotlin

* Use loginId from MessageBox address book

* Revert using compatible HTML mode in Vulcan

* Implement message meta and read status changing

* Always set attachment lists

* Fix setting tutor role description

* Implement sending messages

* Replace millis constant with WEEK

* Revert timetable changes

* Remove unused DataVulcan properties

* Ensure UUID-format recipient IDs

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2022-09-17 12:31:35 +02:00
54a61c6254 [Gradle] Update dependencies, update target SDK to 33 2022-09-09 12:47:20 +02:00
ac10874bf1 [4.11.6] Update build.gradle, signing and changelog. 2022-05-27 21:09:40 +02:00
fa55b4901a [Messages/Librus] Disable text styling. (#130) 2022-05-27 18:55:24 +02:00
0f90430387 [4.11.5] Update build.gradle, signing and changelog. 2022-04-18 11:31:24 +02:00
745523c620 [UI] Fix touch events on swipe refresh pages (API 32+). (#129)
* [Hotfix] Fix touch events on swipe refresh pages (API 32)

* Add API SDK version check
2022-04-18 11:28:34 +02:00
8911ce2bc1 [4.11.4] Update build.gradle, signing and changelog. 2022-03-14 17:45:11 +01:00
2990fc5479 [Firebase] Disable notifications for past shared events. 2022-03-14 17:08:35 +01:00
48b7adb564 [UI/Grades] Group unknown subjects without specified name. 2022-03-14 17:05:16 +01:00
146 changed files with 4821 additions and 2456 deletions

View File

@ -13,6 +13,7 @@
<w>synergia</w>
<w>szczodrzyński</w>
<w>szkolny</w>
<w>usos</w>
</words>
</dictionary>
</component>

View File

@ -70,9 +70,11 @@ android {
sourceSets {
unofficial {
java.srcDirs = ["src/main/java", "src/play-not/java"]
manifest.srcFile("src/play-not/AndroidManifest.xml")
}
official {
java.srcDirs = ["src/main/java", "src/play-not/java"]
manifest.srcFile("src/play-not/AndroidManifest.xml")
}
play {
java.srcDirs = ["src/main/java", "src/play/java"]
@ -82,9 +84,6 @@ android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
lintOptions {
checkReleaseBuilds = false
}
buildFeatures {
dataBinding = true
viewBinding = true
@ -98,7 +97,9 @@ android {
jvmTarget = "1.8"
}
packagingOptions {
exclude 'META-INF/library-core_release.kotlin_module'
resources {
excludes += ['META-INF/library-core_release.kotlin_module']
}
}
externalNativeBuild {
cmake {
@ -106,6 +107,9 @@ android {
version "3.10.2"
}
}
lint {
checkReleaseBuilds false
}
}
tasks.whenTaskAdded { task ->
@ -140,28 +144,29 @@ dependencies {
// Language cores
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.multidex:multidex:2.0.1"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
// Android Jetpack
implementation "androidx.appcompat:appcompat:1.3.1"
implementation "androidx.appcompat:appcompat:1.5.1"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.1"
implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.core:core-ktx:1.9.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.room:room-runtime:2.3.0"
implementation "androidx.work:work-runtime-ktx:2.6.0"
kapt "androidx.room:room-compiler:2.3.0"
implementation "androidx.room:room-runtime:2.4.3"
implementation "androidx.work:work-runtime-ktx:2.7.1"
kapt "androidx.room:room-compiler:2.4.3"
// Google design libs
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.android.material:material:1.6.1"
implementation "com.google.android.flexbox:flexbox:3.0.0"
// Play Services/Firebase
implementation "com.google.android.gms:play-services-wearable:17.1.0"
implementation "com.google.firebase:firebase-core:19.0.2"
implementation "com.google.firebase:firebase-crashlytics:18.2.3"
implementation("com.google.firebase:firebase-core") { version { strictly "19.0.2" } }
implementation "com.google.firebase:firebase-crashlytics:18.2.13"
implementation("com.google.firebase:firebase-messaging") { version { strictly "20.1.3" } }
// OkHttp, Retrofit, Gson, Jsoup

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="pl.szczodrzynski.edziennik">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@ -13,7 +12,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- PowerPermission uses minSdk 21, it's safe to override as it is used only in >= 23 -->
<uses-sdk tools:overrideLibrary="com.qifan.powerpermission.coroutines, com.qifan.powerpermission.core" />
@ -43,6 +42,7 @@
android:configChanges="orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:exported="true"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -66,6 +66,7 @@
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:noHistory="true"
android:exported="true"
android:theme="@style/AppTheme.Dark.NoDisplay">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
@ -73,7 +74,8 @@
</activity>
<!-- TIMETABLE -->
<receiver android:name=".ui.widgets.timetable.WidgetTimetableProvider"
android:label="@string/widget_timetable_title">
android:label="@string/widget_timetable_title"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@ -88,10 +90,12 @@
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:noHistory="true"
android:exported="true"
android:theme="@style/AppTheme.Dark.NoDisplay" />
<!-- NOTIFICATIONS -->
<receiver android:name=".ui.widgets.notifications.WidgetNotificationsProvider"
android:label="@string/widget_notifications_title">
android:label="@string/widget_notifications_title"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@ -104,7 +108,8 @@
android:permission="android.permission.BIND_REMOTEVIEWS" />
<!-- LUCKY NUMBER -->
<receiver android:name=".ui.widgets.luckynumber.WidgetLuckyNumberProvider"
android:label="@string/widget_lucky_number_title">
android:label="@string/widget_lucky_number_title"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@ -124,29 +129,40 @@
<activity android:name=".ui.base.CrashActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:process=":error_activity"
android:exported="false"
android:theme="@style/DeadTheme" />
<activity android:name=".ui.intro.ChangelogIntroActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name"
android:exported="false"
android:theme="@style/Theme.Intro" />
<activity android:name=".ui.login.LoginActivity"
android:configChanges="orientation|screenSize"
android:launchMode="singleTop"
android:exported="false"
android:theme="@style/AppTheme.Light" />
<activity android:name=".ui.home.CounterActivity"
android:exported="false"
android:theme="@style/AppTheme.Black" />
<activity android:name=".ui.feedback.FeedbackActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/app_name"
android:exported="false"
android:theme="@style/AppTheme" />
<activity android:name=".ui.settings.SettingsLicenseActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/AppTheme" />
<activity android:name="com.canhub.cropper.CropImageActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/Base.Theme.AppCompat" />
<activity android:name=".ui.base.BuildInvalidActivity" />
<activity android:name=".ui.settings.contributors.ContributorsActivity" />
<activity android:name=".ui.login.oauth.OAuthLoginActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/AppTheme.Light" />
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />
<!-- _____ _
| __ \ (_)
@ -156,12 +172,14 @@
|_| \_\___|\___\___|_| \_/ \___|_| |___/
-->
<receiver android:name=".receivers.UserPresentReceiver"
android:enabled="true">
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.USER_PRESENT" />
</intent-filter>
</receiver>
<receiver android:name=".sync.UpdateDownloaderService$DownloadProgressReceiver">
<receiver android:name=".sync.UpdateDownloaderService$DownloadProgressReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>

View File

@ -1,6 +1,9 @@
<h3>Wersja 4.11.3, 2022-02-21</h3>
<h3>Wersja 4.13-beta.1, 2022-10-17</h3>
<ul>
<li>Naprawiono odświeżanie planu lekcji po pobraniu wybranego tygodnia.</li>
<li>Poprawione powiadomienia na Androidzie 13. @santoni0</li>
<li>Możliwość dostosowania wyświetlania planu lekcji</li>
<li>Opcja kolorowania bloków w planie lekcji</li>
<li><b>USOS</b> - pierwsza wersja obsługi systemu</li>
</ul>
<br>
<br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0x90, 0xe9, 0x2f, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0x12, 0xdd, 0xb2, 0x93, 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

@ -34,6 +34,7 @@ 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_REQUIRES_USER_ACTION
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.*
@ -69,6 +70,7 @@ import pl.szczodrzynski.edziennik.ui.grades.editor.GradesEditorFragment
import pl.szczodrzynski.edziennik.ui.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.homework.HomeworkFragment
import pl.szczodrzynski.edziennik.ui.login.LoginActivity
import pl.szczodrzynski.edziennik.ui.login.LoginProgressFragment
import pl.szczodrzynski.edziennik.ui.messages.compose.MessagesComposeFragment
import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment
import pl.szczodrzynski.edziennik.ui.messages.single.MessageFragment
@ -83,6 +85,7 @@ import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.NavTarget
import pl.szczodrzynski.navlib.*
@ -853,7 +856,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
app.userActionManager.execute(this, event.profileId, event.type)
app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
}
private fun fragmentToSyncName(currentFragment: Int): Int {
@ -911,11 +914,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
false
}
"userActionRequired" -> {
app.userActionManager.execute(
this,
extras.getInt("profileId"),
extras.getInt("type")
val event = UserActionRequiredEvent(
profileId = extras.getInt("profileId"),
type = extras.getEnum<UserActionRequiredEvent.Type>("type") ?: return,
params = extras.getBundle("params") ?: return,
errorText = 0,
)
app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
true
}
"createManualEvent" -> {

View File

@ -69,4 +69,24 @@ class ProfileConfigUI(private val config: ProfileConfig) {
var messagesGreetingText: String?
get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText }
set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value }
private var mTimetableShowAttendance: Boolean? = null
var timetableShowAttendance: Boolean
get() { mTimetableShowAttendance = mTimetableShowAttendance ?: config.values.get("timetableShowAttendance", true); return mTimetableShowAttendance ?: true }
set(value) { config.set("timetableShowAttendance", value); mTimetableShowAttendance = value }
private var mTimetableShowEvents: Boolean? = null
var timetableShowEvents: Boolean
get() { mTimetableShowEvents = mTimetableShowEvents ?: config.values.get("timetableShowEvents", true); return mTimetableShowEvents ?: true }
set(value) { config.set("timetableShowEvents", value); mTimetableShowEvents = value }
private var mTimetableTrimHourRange: Boolean? = null
var timetableTrimHourRange: Boolean
get() { mTimetableTrimHourRange = mTimetableTrimHourRange ?: config.values.get("timetableTrimHourRange", false); return mTimetableTrimHourRange ?: false }
set(value) { config.set("timetableTrimHourRange", value); mTimetableTrimHourRange = value }
private var mTimetableColorSubjectName: Boolean? = null
var timetableColorSubjectName: Boolean
get() { mTimetableColorSubjectName = mTimetableColorSubjectName ?: config.values.get("timetableColorSubjectName", false); return mTimetableColorSubjectName ?: false }
set(value) { config.set("timetableColorSubjectName", value); mTimetableColorSubjectName = value }
}

View File

@ -84,19 +84,21 @@ class ApiService : Service() {
runTask()
}
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
app.userActionManager.sendToUser(event)
taskRunning?.cancel()
clearTask()
runTask()
}
override fun onError(apiError: ApiError) {
lastEventTime = System.currentTimeMillis()
d(TAG, "Task $taskRunningId threw an error - $apiError")
apiError.profileId = taskProfileId
if (app.userActionManager.requiresUserAction(apiError)) {
app.userActionManager.sendToUser(apiError)
}
else {
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
errorList.add(apiError)
apiError.throwable?.printStackTrace()
}
EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError))
errorList.add(apiError)
apiError.throwable?.printStackTrace()
if (apiError.isCritical) {
taskRunning?.cancel()

View File

@ -59,40 +59,15 @@ 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"
const val IDZIENNIK_WEB_LOGIN = "login.aspx"
const val IDZIENNIK_WEB_SETTINGS = "mod_panelRodzica/Ustawienia.aspx"
const val IDZIENNIK_WEB_HOME = "mod_panelRodzica/StronaGlowna.aspx"
const val IDZIENNIK_WEB_TIMETABLE = "mod_panelRodzica/plan/WS_Plan.asmx/pobierzPlanZajec"
const val IDZIENNIK_WEB_GRADES = "mod_panelRodzica/oceny/WS_ocenyUcznia.asmx/pobierzOcenyUcznia"
const val IDZIENNIK_WEB_MISSING_GRADES = "mod_panelRodzica/brak_ocen/WS_BrakOcenUcznia.asmx/pobierzBrakujaceOcenyUcznia"
const val IDZIENNIK_WEB_EXAMS = "mod_panelRodzica/sprawdziany/mod_sprawdzianyPanel.asmx/pobierzListe"
const val IDZIENNIK_WEB_HOMEWORK = "mod_panelRodzica/pracaDomowa/WS_pracaDomowa.asmx/pobierzPraceDomowe"
const val IDZIENNIK_WEB_NOTICES = "mod_panelRodzica/uwagi/WS_uwagiUcznia.asmx/pobierzUwagiUcznia"
const val IDZIENNIK_WEB_ATTENDANCE = "mod_panelRodzica/obecnosci/WS_obecnosciUcznia.asmx/pobierzObecnosciUcznia"
const val IDZIENNIK_WEB_ANNOUNCEMENTS = "mod_panelRodzica/tabOgl/WS_tablicaOgloszen.asmx/GetOgloszenia"
const val IDZIENNIK_WEB_MESSAGES_LIST = "mod_komunikator/WS_wiadomosci.asmx/PobierzListeWiadomosci"
const val IDZIENNIK_WEB_GET_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/PobierzWiadomosc"
const val IDZIENNIK_WEB_GET_RECIPIENT_LIST = "mod_komunikator/WS_wiadomosci.asmx/pobierzListeOdbiorcowPanelRodzic"
const val IDZIENNIK_WEB_SEND_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/WyslijWiadomosc"
const val IDZIENNIK_WEB_GET_ATTACHMENT = "mod_komunikator/Download.ashx"
const val IDZIENNIK_WEB_GET_HOMEWORK = "mod_panelRodzica/pracaDomowa/WS_pracaDomowa.asmx/pobierzJednaPraceDomowa"
const val IDZIENNIK_WEB_GET_HOMEWORK_ATTACHMENT = "mod_panelRodzica/pracaDomowa.aspx"
val IDZIENNIK_API_USER_AGENT = SYSTEM_USER_AGENT
const val IDZIENNIK_API_URL = "https://iuczniowie.progman.pl/idziennik/api"
const val IDZIENNIK_API_CURRENT_REGISTER = "Uczniowie/\$STUDENT_ID/AktualnyDziennik"
const val IDZIENNIK_API_GRADES = "Uczniowie/\$STUDENT_ID/Oceny/" /* + semester */
const val IDZIENNIK_API_MESSAGES_INBOX = "Wiadomosci/Odebrane"
const val IDZIENNIK_API_MESSAGES_SENT = "Wiadomosci/Wyslane"
const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR"
const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login"
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
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)"
const val VULCAN_HEBE_APP_VERSION = "22.09.02 (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 {
@ -116,14 +91,23 @@ 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_MESSAGEBOX = "api/mobile/messagebox"
const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK = "api/mobile/messagebox/addressbook"
const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_MESSAGES = "api/mobile/messagebox/message"
const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_STATUS = "api/mobile/messagebox/message/status"
const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_SEND = "api/mobile/messagebox/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"
const val USOS_API_OAUTH_REDIRECT_URL = "szkolny://redirect/usos"
val USOS_API_SCOPES by lazy { listOf(
"offline_access",
"studies",
"grades",
"events",
) }

View File

@ -13,6 +13,7 @@ import androidx.core.app.NotificationCompat.PRIORITY_MIN
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.ext.Bundle
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver
import kotlin.math.roundToInt
@ -40,14 +41,14 @@ class EdziennikNotification(val app: App) {
"task" to "TaskCancelRequest",
"taskId" to taskId
))
return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) as PendingIntent
return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag()) as PendingIntent
}
private val closePendingIntent: PendingIntent
get() {
val intent = SzkolnyReceiver.getIntent(app, Bundle(
"task" to "ServiceCloseRequest"
))
return PendingIntent.getBroadcast(app, 0, intent, 0) as PendingIntent
return PendingIntent.getBroadcast(app, 0, intent, pendingIntentFlag()) as PendingIntent
}
private fun errorCountText(): String? {

View File

@ -58,11 +58,7 @@ const val ERROR_INVALID_LOGIN_MODE = 110
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
const val ERROR_NOT_IMPLEMENTED = 112
const val ERROR_FILE_DOWNLOAD = 113
const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115
const val ERROR_CAPTCHA_NEEDED = 3000
const val ERROR_CAPTCHA_LIBRUS_PORTAL = 3001
const val ERROR_REQUIRES_USER_ACTION = 114
const val ERROR_API_PDO_ERROR = 5000
const val ERROR_API_INVALID_CLIENT = 5001
@ -198,19 +194,18 @@ const val ERROR_VULCAN_HEBE_ENTITY_NOT_FOUND = 365
const val ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY = 366
const val ERROR_VULCAN_API_DEPRECATED = 390
const val ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN = 501
const val ERROR_LOGIN_EDUDZIENNIK_WEB_OTHER = 510
const val ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID = 511
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_USOS_OAUTH_GOT_DIFFERENT_TOKEN = 702
const val ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE = 703
const val ERROR_USOS_NO_STUDENT_PROGRAMMES = 704
const val ERROR_USOS_API_INCOMPLETE_RESPONSE = 705
const val ERROR_USOS_API_MISSING_RESPONSE = 706
const val ERROR_TEMPLATE_WEB_OTHER = 801
const val EXCEPTION_API_TASK = 900
@ -224,8 +219,6 @@ const val EXCEPTION_MOBIDZIENNIK_WEB_FILE_REQUEST = 908
const val EXCEPTION_LIBRUS_MESSAGES_FILE_REQUEST = 909
const val EXCEPTION_NOTIFY = 910
const val EXCEPTION_LIBRUS_MESSAGES_REQUEST = 911
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

View File

@ -4,7 +4,6 @@
package pl.szczodrzynski.edziennik.data.api
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginMessages
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginPortal
@ -14,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.Mobidzie
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.usos.login.UsosLoginApi
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
@ -26,8 +26,9 @@ import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
const val SYNERGIA_API_ENABLED = false
// the graveyard
const val LOGIN_TYPE_IDZIENNIK = 3
const val LOGIN_TYPE_EDUDZIENNIK = 5
const val LOGIN_TYPE_TEMPLATE = 21
@ -118,15 +119,6 @@ val vulcanLoginMethods = listOf(
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
)
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)
.withIsPossible { _, _ -> true }
.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
@ -136,6 +128,15 @@ val podlasieLoginMethods = listOf(
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
)
const val LOGIN_TYPE_USOS = 7
const val LOGIN_MODE_USOS_OAUTH = 0
const val LOGIN_METHOD_USOS_API = 100
val usosLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_USOS, LOGIN_METHOD_USOS_API, UsosLoginApi::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

@ -20,6 +20,10 @@ object Regexes {
"""<br\s?/?>""".toRegex()
}
val MESSAGE_META by lazy {
"""^\[META:([A-z0-9-&=]+)]""".toRegex()
}
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {
@ -142,44 +146,6 @@ object Regexes {
}
val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy {
"""<input type="hidden".+?name="([A-z0-9_]+)?".+?value="([A-z0-9_+-/=]+)?".+?>""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_ERROR by lazy {
"""id="spanErrorMessage">(.*?)</""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_FIRST_ACCOUNT_NAME by lazy {
"""Imię i nazwisko:.+?">(.+?)</div>""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_FIRST_IS_PARENT by lazy {
"""id="ctl00_CzyRodzic" value="([01])" />""".toRegex()
}
val IDZIENNIK_LOGIN_FIRST_SCHOOL_YEAR by lazy {
"""name="ctl00\${"$"}dxComboRokSzkolny".+?selected="selected".*?value="([0-9]+)">([0-9]+)/([0-9]+)<""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_FIRST_STUDENT_SELECT by lazy {
"""<select.*?name="ctl00\${"$"}dxComboUczniowie".*?</select>""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_FIRST_STUDENT by lazy {
"""<option.*?value="([0-9]+)"\sdata-id-ucznia="([A-z0-9]+?)".*?>(.+?)\s(.+?)\s*\((.+?),\s*(.+?)\)</option>""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_MESSAGES_RECIPIENT_PARENT by lazy {
"""(.+?)\s\((.+)\)""".toRegex()
}
/*<span id="ctl00_spanSzczesliwyLos">Szczęśliwy los na dzisiaj to <b>19</b>. Los na jutro to <b>22</b></span>*/
val IDZIENNIK_WEB_LUCKY_NUMBER by lazy {
"""dzisiaj to <b>([0-9]+)</b>""".toRegex()
}
val IDZIENNIK_WEB_SELECTED_REGISTER by lazy {
"""selected="selected" value="([0-9]+)" data-id-ucznia""".toRegex()
}
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()
}
@ -197,82 +163,6 @@ object Regexes {
}
val EDUDZIENNIK_STUDENTS_START by lazy {
"""<li><a href="/Students/([\w-_]+?)/start/">(.*?)</a>""".toRegex()
}
val EDUDZIENNIK_ACCOUNT_NAME_START by lazy {
"""<span id='user_dn'>(.*?)</span>""".toRegex()
}
val EDUDZIENNIK_SUBJECTS_START by lazy {
"""<a class="menu-course" href="/Students/[\w-_]+?/Courses/([\w-_]+)/">(.+?)</a>""".toRegex()
}
val EDUDZIENNIK_ATTENDANCE_ENTRIES by lazy {
"""<td id="([\d-]+?):(\d+?)".*?>(.+?)</td>""".toRegex()
}
val EDUDZIENNIK_ATTENDANCE_TYPES by lazy {
"""<div class="info">.*?<p>(.*?)</p>""".toRegex(DOT_MATCHES_ALL)
}
val EDUDZIENNIK_ATTENDANCE_TYPE by lazy {
"""\((.+?)\) (.+)""".toRegex()
}
val EDUDZIENNIK_ANNOUNCEMENT_DESCRIPTION by lazy {
"""<div class="desc">.*?<p>(.*?)</p>""".toRegex(DOT_MATCHES_ALL)
}
val EDUDZIENNIK_HOMEWORK_DESCRIPTION by lazy {
"""<div class="desc">(.*?)</div>""".toRegex(DOT_MATCHES_ALL)
}
val EDUDZIENNIK_SUBJECT_ID by lazy {
"""/Courses/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_GRADE_ID by lazy {
"""/Grades/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_EXAM_ID by lazy {
"""/Evaluations/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_EVENT_TYPE_ID by lazy {
"""/GradeLabels/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_ANNOUNCEMENT_ID by lazy {
"""/Announcement/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_HOMEWORK_ID by lazy {
"""/Homework/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_TEACHER_ID by lazy {
"""/Teachers/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_EVENT_ID by lazy {
"""/KlassEvent/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_NOTE_ID by lazy {
"""/RegistryNotes/([\w-_]+?)/""".toRegex()
}
val EDUDZIENNIK_SCHOOL_DETAIL_ID by lazy {
"""<a id="School_detail".*?/School/([\w-_]+?)/""".toRegex(DOT_MATCHES_ALL)
}
val EDUDZIENNIK_SCHOOL_DETAIL_NAME by lazy {
"""</li>.*?<p>(.*?)</p>.*?<li>""".toRegex(DOT_MATCHES_ALL)
}
val EDUDZIENNIK_CLASS_DETAIL_ID by lazy {
"""<a id="Klass_detail".*?/Klass/([\w-_]+?)/""".toRegex(DOT_MATCHES_ALL)
}
val EDUDZIENNIK_CLASS_DETAIL_NAME by lazy {
"""<a id="Klass_detail".*?>(.*?)</a>""".toRegex(DOT_MATCHES_ALL)
}
val EDUDZIENNIK_TEACHERS by lazy {
"""<div class="teacher">.*?<p>(.+?) (.+?)</p>""".toRegex(DOT_MATCHES_ALL)
}
val LINKIFY_DATE_YMD by lazy {
"""(1\d{3}|20\d{2})[\-./](1[0-2]|0?\d)[\-./]([1-2]\d|3[0-1]|0?\d)""".toRegex()
}

View File

@ -8,11 +8,11 @@ import com.google.gson.JsonObject
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.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.usos.Usos
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
@ -112,9 +112,9 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
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_EDUDZIENNIK -> Edudziennik(app, profile, loginStore, taskCallback)
LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback)
LOGIN_TYPE_USOS -> Usos(app, profile, loginStore, taskCallback)
else -> null
}
if (edziennikInterface == null) {

View File

@ -1,125 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-22
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_EDUDZIENNIK_WEB
import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.ext.*
/**
* Use http://patorjk.com/software/taag/#p=display&f=Big for the ascii art
*
* Use https://codepen.io/kubasz/pen/RwwwbGN to easily generate the student data getters/setters
*/
class DataEdudziennik(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
fun isWebLoginValid() = webSessionIdExpiryTime-30 > currentTimeUnix() && webSessionId.isNotNullNorEmpty()
override fun satisfyLoginMethods() {
loginMethods.clear()
if (isWebLoginValid()) {
loginMethods += LOGIN_METHOD_EDUDZIENNIK_WEB
}
}
override fun generateUserCode() = "$schoolName:$loginEmail:${studentId?.crc32()}"
private var mLoginEmail: String? = null
var loginEmail: String?
get() { mLoginEmail = mLoginEmail ?: loginStore.getLoginData("email", null); return mLoginEmail }
set(value) { loginStore.putLoginData("email", value); mLoginEmail = value }
private var mLoginPassword: String? = null
var loginPassword: String?
get() { mLoginPassword = mLoginPassword ?: loginStore.getLoginData("password", null); return mLoginPassword }
set(value) { loginStore.putLoginData("password", value); mLoginPassword = 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 mSchoolId: String? = null
var schoolId: String?
get() { mSchoolId = mSchoolId ?: profile?.getStudentData("schoolId", null); return mSchoolId }
set(value) { profile?.putStudentData("schoolId", value) ?: return; mSchoolId = value }
private var mClassId: String? = null
var classId: String?
get() { mClassId = mClassId ?: profile?.getStudentData("classId", null); return mClassId }
set(value) { profile?.putStudentData("classId", value) ?: return; mClassId = value }
/* __ __ _
\ \ / / | |
\ \ /\ / /__| |__
\ \/ \/ / _ \ '_ \
\ /\ / __/ |_) |
\/ \/ \___|_._*/
private var mWebSessionId: String? = null
var webSessionId: String?
get() { mWebSessionId = mWebSessionId ?: loginStore.getLoginData("webSessionId", null); return mWebSessionId }
set(value) { loginStore.putLoginData("webSessionId", value); mWebSessionId = value }
private var mWebSessionIdExpiryTime: Long? = null
var webSessionIdExpiryTime: Long
get() { mWebSessionIdExpiryTime = mWebSessionIdExpiryTime ?: loginStore.getLoginData("webSessionIdExpiryTime", 0L); return mWebSessionIdExpiryTime ?: 0L }
set(value) { loginStore.putLoginData("webSessionIdExpiryTime", value); mWebSessionIdExpiryTime = value }
/* ____ _ _
/ __ \| | | |
| | | | |_| |__ ___ _ __
| | | | __| '_ \ / _ \ '__|
| |__| | |_| | | | __/ |
\____/ \__|_| |_|\___|*/
private var mCurrentSemester: Int? = null
var currentSemester: Int
get() { mCurrentSemester = mCurrentSemester ?: profile?.getStudentData("currentSemester", 1); return mCurrentSemester ?: 1 }
set(value) { profile?.putStudentData("currentSemester", value) ?: return; mCurrentSemester = 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 }
val studentEndpoint: String
get() = "Students/$studentId/"
val schoolEndpoint: String
get() = "Schools/$schoolId/"
val classStudentEndpoint: String
get() = "Class/$studentId/"
val schoolClassEndpoint: String
get() = "Schools/$classId/"
val studentAndClassEndpoint: String
get() = "Students/$studentId/Klass/$classId/"
val studentAndClassesEndpoint: String
get() = "Students/$studentId/Classes/$classId/"
val timetableEndpoint: String
get() = "Plan/$studentId/"
val studentAndTeacherClassEndpoint: String
get() = "Students/$studentId/Teachers/$classId/"
val courseStudentEndpoint: String
get() = "Course/$studentId/"
fun getEventType(longId: String, name: String): EventType {
val id = longId.crc16().toLong()
return eventTypes.singleOrNull { it.id == id } ?: run {
val eventType = EventType(profileId, id, name, colorFromName(name))
eventTypes.put(id, eventType)
eventType
}
}
}

View File

@ -1,142 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-22
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikData
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.EdudziennikWebGetAnnouncement
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.EdudziennikWebGetHomework
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.firstlogin.EdudziennikFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb
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.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
class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
companion object {
private const val TAG = "Edudziennik"
}
val internalErrorList = mutableListOf<Int>()
val data: DataEdudziennik
private var afterLogin: (() -> Unit)? = null
init {
data = DataEdudziennik(app, profile, loginStore).apply {
callback = wrapCallback(this@Edudziennik.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(edudziennikLoginMethods, EdudziennikFeatures, featureIds, viewId, onlyEndpoints)
login()
}
private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) {
d(TAG, "Trying to login with ${data.targetLoginMethodIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
loginMethodId?.let { data.prepareFor(edudziennikLoginMethods, it) }
afterLogin?.let { this.afterLogin = it }
EdudziennikLogin(data) {
data()
}
}
private fun data() {
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
if (internalErrorList.isNotEmpty()) {
d(TAG, " - Internal errors:")
internalErrorList.forEach { d(TAG, " - code $it") }
}
afterLogin?.invoke() ?: EdudziennikData(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) {
EdudziennikLoginWeb(data) {
EdudziennikWebGetAnnouncement(data, announcement) {
completed()
}
}
}
override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {}
override fun getRecipientList() {}
override fun getEvent(eventFull: EventFull) {
EdudziennikLoginWeb(data) {
EdudziennikWebGetHomework(data, eventFull) {
completed()
}
}
}
override fun firstLogin() { EdudziennikFirstLogin(data) { completed() } }
override fun cancel() {
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) {
if (apiError.errorCode in internalErrorList) {
// finish immediately if the same error occurs twice during the same sync
callback.onError(apiError)
return
}
internalErrorList.add(apiError.errorCode)
when (apiError.errorCode) {
ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED -> {
login()
}
ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID -> {
login()
}
ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS -> {
data()
}
else -> callback.onError(apiError)
}
}
}
}
}

View File

@ -1,74 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-23
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.models.Feature
const val ENDPOINT_EDUDZIENNIK_WEB_START = 1000
const val ENDPOINT_EDUDZIENNIK_WEB_TEACHERS = 1001
const val ENDPOINT_EDUDZIENNIK_WEB_GRADES = 1011
const val ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE = 1012
const val ENDPOINT_EDUDZIENNIK_WEB_EXAMS = 1013
const val ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE = 1014
const val ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS = 1015
const val ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK = 1016
const val ENDPOINT_EDUDZIENNIK_WEB_EVENTS = 1017
const val ENDPOINT_EDUDZIENNIK_WEB_NOTES = 1018
const val ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER = 1030
val EdudziennikFeatures = listOf(
/* School and team info and subjects */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_STUDENT_INFO, listOf(
ENDPOINT_EDUDZIENNIK_WEB_START to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Teachers */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_TEACHERS, listOf(
ENDPOINT_EDUDZIENNIK_WEB_TEACHERS to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Timetable */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_TIMETABLE, listOf(
ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Grades */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_GRADES, listOf(
ENDPOINT_EDUDZIENNIK_WEB_GRADES to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Agenda */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_AGENDA, listOf(
ENDPOINT_EDUDZIENNIK_WEB_EXAMS to LOGIN_METHOD_EDUDZIENNIK_WEB,
ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK to LOGIN_METHOD_EDUDZIENNIK_WEB,
ENDPOINT_EDUDZIENNIK_WEB_EVENTS to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Homework */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_HOMEWORK, listOf(
ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Behaviour */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_BEHAVIOUR, listOf(
ENDPOINT_EDUDZIENNIK_WEB_NOTES to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Attendance */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_ATTENDANCE, listOf(
ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Announcements */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_ANNOUNCEMENTS, listOf(
ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)),
/* Lucky number */
Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_LUCKY_NUMBER, listOf(
ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER to LOGIN_METHOD_EDUDZIENNIK_WEB
), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB))
)

View File

@ -1,88 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-22
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.*
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.*
import pl.szczodrzynski.edziennik.utils.Utils
class EdudziennikData(val data: DataEdudziennik, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "EdudziennikData"
}
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) { endpointId ->
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_EDUDZIENNIK_WEB_START -> {
data.startProgress(R.string.edziennik_progress_endpoint_data)
EdudziennikWebStart(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_TEACHERS -> {
data.startProgress(R.string.edziennik_progress_endpoint_teachers)
EdudziennikWebTeachers(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_GRADES -> {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
EdudziennikWebGrades(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE -> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
EdudziennikWebTimetable(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_EXAMS -> {
data.startProgress(R.string.edziennik_progress_endpoint_exams)
EdudziennikWebExams(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE -> {
data.startProgress(R.string.edziennik_progress_endpoint_attendance)
EdudziennikWebAttendance(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_announcements)
EdudziennikWebAnnouncements(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK -> {
data.startProgress(R.string.edziennik_progress_endpoint_homework)
EdudziennikWebHomework(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_EVENTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_events)
EdudziennikWebEvents(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_NOTES -> {
data.startProgress(R.string.edziennik_progress_endpoint_notices)
EdudziennikWebNotes(data, lastSync, onSuccess)
}
ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER -> {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
EdudziennikWebLuckyNumber(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}
}

View File

@ -1,90 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-22
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
open class EdudziennikWeb(open val data: DataEdudziennik, open val lastSync: Long?) {
companion object {
private const val TAG = "EdudziennikWeb"
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
fun webGet(tag: String, endpoint: String, xhr: Boolean = false, semester: Int? = null, onSuccess: (text: String) -> Unit) {
val url = "https://dziennikel.appspot.com/" + when (endpoint.endsWith('/') || endpoint.contains('?') || endpoint.isEmpty()) {
true -> endpoint
else -> "$endpoint/"
} + (semester?.let { "?semester=" + if(it == -1) "all" else it } ?: "")
d(tag, "Request: Edudziennik/Web - $url")
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (text == null || response == null) {
data.error(ApiError(tag, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
if (semester == null && url.contains("start")) {
profile?.also { profile ->
val cookies = data.app.cookieJar.getAll("dziennikel.appspot.com")
val semesterCookie = cookies["semester"]?.toIntOrNull()
semesterCookie?.let { data.currentSemester = it }
if (semesterCookie == 2 && profile.dateSemester2Start > Date.getToday())
profile.dateSemester2Start = Date.getToday().stepForward(0, 0, -1)
}
}
try {
onSuccess(text)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_EDUDZIENNIK_WEB_REQUEST)
.withThrowable(e)
.withResponse(response)
.withApiResponse(text))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
val error = when (response?.code()) {
402 -> ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS
403 -> ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED
else -> ERROR_REQUEST_FAILURE
}
data.error(ApiError(tag, error)
.withResponse(response)
.withThrowable(throwable))
}
}
data.app.cookieJar.set("dziennikel.appspot.com", "sessionid", data.webSessionId)
Request.builder()
.url(url)
.userAgent(EDUDZIENNIK_USER_AGENT)
.apply {
if (xhr) header("X-Requested-With", "XMLHttpRequest")
}
.get()
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -1,75 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-26
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ANNOUNCEMENT_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.db.entity.Announcement
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.utils.models.Date
class EdudziennikWebAnnouncements(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
const val TAG = "EdudziennikWebAnnouncements"
}
init { data.profile?.also { profile ->
webGet(TAG, data.schoolClassEndpoint + "Announcements") { text ->
val doc = Jsoup.parse(text)
if (doc.getElementsByClass("message").text().trim() != "Brak ogłoszeń.") {
doc.select("table.list tbody tr").forEach { announcementElement ->
val titleElement = announcementElement.child(0).child(0)
val longId = EDUDZIENNIK_ANNOUNCEMENT_ID.find(titleElement.attr("href"))?.get(1)
?: return@forEach
val id = longId.crc32()
val subject = titleElement.text()
val teacherName = announcementElement.child(1).text()
val teacher = data.getTeacherByFirstLast(teacherName)
val dateString = announcementElement.getElementsByClass("datetime").first()?.text()
val startDate = Date.fromY_m_d(dateString)
val addedDate = Date.fromIsoHm(dateString)
val announcementObject = Announcement(
profileId = profileId,
id = id,
subject = subject,
text = null,
startDate = startDate,
endDate = null,
teacherId = teacher.id,
addedDate = addedDate
).also {
it.idString = longId
}
data.announcementList.add(announcementObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_ANNOUNCEMENT,
id,
profile.empty,
profile.empty
))
}
}
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS)
}
} ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS) }
}

View File

@ -1,114 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-24
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_ENTRIES
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_TYPE
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_TYPES
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
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
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
import java.util.*
class EdudziennikWebAttendance(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
private const val TAG = "EdudziennikWebAttendance"
}
private var requestSemester: Int? = null
init {
if (profile?.empty == true && data.currentSemester == 2) requestSemester = 1
getAttendances()
}
private fun getAttendances() { data.profile?.also { profile ->
webGet(TAG, data.studentEndpoint + "Presence", semester = requestSemester) { text ->
val attendanceTypes = EDUDZIENNIK_ATTENDANCE_TYPES.find(text)?.get(1)?.split(',')?.map {
val type = EDUDZIENNIK_ATTENDANCE_TYPE.find(it.trim())
val symbol = type?.get(1)?.trim() ?: "?"
val name = type?.get(2)?.trim() ?: "nieznany rodzaj"
return@map Triple(
symbol,
name,
when (name.lowercase()) {
"obecność" -> Attendance.TYPE_PRESENT
"nieobecność" -> Attendance.TYPE_ABSENT
"spóźnienie" -> Attendance.TYPE_BELATED
"nieobecność usprawiedliwiona" -> Attendance.TYPE_ABSENT_EXCUSED
"dzień wolny" -> Attendance.TYPE_DAY_FREE
"brak zajęć" -> Attendance.TYPE_DAY_FREE
"oddelegowany" -> Attendance.TYPE_RELEASED
else -> Attendance.TYPE_UNKNOWN
}
)
} ?: emptyList()
EDUDZIENNIK_ATTENDANCE_ENTRIES.findAll(text).forEach { attendanceElement ->
val date = Date.fromY_m_d(attendanceElement[1])
val lessonNumber = attendanceElement[2].toInt()
val attendanceSymbol = attendanceElement[3]
val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date)
val lesson = lessons.firstOrNull { it.lessonNumber == lessonNumber }
val id = "${date.stringY_m_d}:$lessonNumber:$attendanceSymbol".crc32()
val (typeSymbol, typeName, baseType) = attendanceTypes.firstOrNull { (symbol, _, _) -> symbol == attendanceSymbol }
?: return@forEach
val startTime = data.lessonRanges.singleOrNull { it.lessonNumber == lessonNumber }?.startTime
?: return@forEach
val attendanceObject = Attendance(
profileId = profileId,
id = id,
baseType = baseType,
typeName = typeName,
typeShort = data.app.attendanceManager.getTypeShort(baseType),
typeSymbol = typeSymbol,
typeColor = null,
date = date,
startTime = lesson?.displayStartTime ?: startTime,
semester = profile.currentSemester,
teacherId = lesson?.displayTeacherId ?: -1,
subjectId = lesson?.displaySubjectId ?: -1
).also {
it.lessonNumber = lessonNumber
}
data.attendanceList.add(attendanceObject)
if (baseType != Attendance.TYPE_PRESENT) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_ATTENDANCE,
id,
profile.empty || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN,
profile.empty || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN
))
}
}
if (profile.empty && requestSemester == 1 && data.currentSemester == 2) {
requestSemester = null
getAttendances()
} else {
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE)
}
}
} ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE) }
}

View File

@ -1,70 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-1-1
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EVENT_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_EVENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
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.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.utils.models.Date
class EdudziennikWebEvents(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
const val TAG = "EdudziennikWebEvents"
}
init { data.profile?.also { profile ->
webGet(TAG, data.studentAndClassesEndpoint + "KlassEvent", xhr = true) { text ->
val doc = Jsoup.parseBodyFragment("<table>" + text.trim() + "</table>")
doc.getElementsByTag("tr").forEach { eventElement ->
val date = Date.fromY_m_d(eventElement.child(1).text())
val titleElement = eventElement.child(2).child(0)
val title = titleElement.text().trim()
val id = EDUDZIENNIK_EVENT_ID.find(titleElement.attr("href"))?.get(1)?.crc32()
?: return@forEach
val eventObject = Event(
profileId = profileId,
id = id,
date = date,
time = null,
topic = title,
color = null,
type = Event.TYPE_CLASS_EVENT,
teacherId = -1,
subjectId = -1,
teamId = data.teamClass?.id ?: -1
)
data.eventList.add(eventObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_EVENT,
id,
profile.empty,
profile.empty
))
}
data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_CLASS_EVENT))
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_EVENTS, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_EVENTS)
}
} ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_EVENTS) }
}

View File

@ -1,90 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-24
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EVENT_TYPE_ID
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EXAM_ID
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_EXAMS
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
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.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.utils.models.Date
class EdudziennikWebExams(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
const val TAG = "EdudziennikWebExams"
}
init { profile?.also { profile ->
webGet(TAG, data.studentAndClassEndpoint + "Evaluations", xhr = true) { text ->
val doc = Jsoup.parseBodyFragment("<table>" + text.trim() + "</table>")
doc.select("tr").forEach { examElement ->
val id = EDUDZIENNIK_EXAM_ID.find(examElement.child(0).child(0).attr("href"))
?.get(1)?.crc32() ?: return@forEach
val topic = examElement.child(0).text().trim()
val subjectElement = examElement.child(1).child(0)
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEach
val subjectName = subjectElement.text().trim()
val subject = data.getSubject(subjectId.crc32(), subjectName)
val dateString = examElement.child(2).text().trim()
if (dateString.isBlank()) return@forEach
val date = Date.fromY_m_d(dateString)
val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date)
val startTime = lessons.firstOrNull { it.displaySubjectId == subject.id }?.displayStartTime
val eventTypeElement = examElement.child(3).child(0)
val eventTypeId = EDUDZIENNIK_EVENT_TYPE_ID.find(eventTypeElement.attr("href"))?.get(1)
?: return@forEach
val eventTypeName = eventTypeElement.text()
val eventType = data.getEventType(eventTypeId, eventTypeName)
val eventObject = Event(
profileId = profileId,
id = id,
date = date,
time = startTime,
topic = topic,
color = null,
type = eventType.id,
teacherId = -1,
subjectId = subject.id,
teamId = data.teamClass?.id ?: -1
)
data.eventList.add(eventObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_EVENT,
id,
profile.empty,
profile.empty
))
}
data.toRemove.add(DataRemoveModel.Events.futureExceptTypes(listOf(
Event.TYPE_HOMEWORK,
Event.TYPE_CLASS_EVENT
)))
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_EXAMS, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_EXAMS)
}
}}
}

View File

@ -1,35 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-26
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.api.events.AnnouncementGetEvent
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.ext.get
class EdudziennikWebGetAnnouncement(override val data: DataEdudziennik,
private val announcement: AnnouncementFull,
val onSuccess: () -> Unit
) : EdudziennikWeb(data, null) {
companion object {
const val TAG = "EdudziennikWebGetAnnouncement"
}
init {
webGet(TAG, "Announcement/${announcement.idString}") { text ->
val description = Regexes.EDUDZIENNIK_ANNOUNCEMENT_DESCRIPTION.find(text)?.get(1)?.trim() ?: ""
announcement.text = description
EventBus.getDefault().postSticky(AnnouncementGetEvent(announcement))
data.announcementList.add(announcement)
onSuccess()
}
}
}

View File

@ -1,47 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
class EdudziennikWebGetHomework(
override val data: DataEdudziennik,
val event: EventFull,
val onSuccess: () -> Unit
) : EdudziennikWeb(data, null) {
companion object {
const val TAG = "EdudziennikWebGetHomework"
}
init {
if (event.attachmentNames.isNotNullNorEmpty()) {
val id = event.attachmentNames!![0]
webGet(TAG, "Homework/$id") { text ->
val description = Regexes.EDUDZIENNIK_HOMEWORK_DESCRIPTION.find(text)?.get(1)?.trim()
if (description != null)
event.topic = BetterHtml.fromHtml(context = null, description).toString()
event.homeworkBody = ""
event.isDownloaded = true
event.attachmentNames = null
data.eventList += event
data.eventListReplace = true
EventBus.getDefault().postSticky(EventGetEvent(event))
onSuccess()
}
} else {
EventBus.getDefault().postSticky(EventGetEvent(event))
onSuccess()
}
}
}

View File

@ -1,230 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-25
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import android.graphics.Color
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
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.Grade.Companion.TYPE_POINT_SUM
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.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.colorFromCssName
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
class EdudziennikWebGrades(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
private const val TAG = "EdudziennikWebGrades"
}
private var requestSemester: Int? = null
init {
if (profile?.empty == true && data.currentSemester == 2) requestSemester = 1
getGrades()
}
private fun getGrades() { data.profile?.also { profile ->
webGet(TAG, data.studentEndpoint + "start", semester = requestSemester) { text ->
val semester = requestSemester ?: data.currentSemester
val doc = Jsoup.parse(text)
val subjects = doc.select("#student_grades tbody").firstOrNull()?.children()
subjects?.forEach { subjectElement ->
if (subjectElement.id().isBlank()) return@forEach
val subjectId = subjectElement.id().trim()
val subjectName = subjectElement.child(0).text().trim()
val subject = data.getSubject(subjectId.crc32(), subjectName)
val gradeType = when {
subjectElement.select("#sum").text().isNotBlank() -> TYPE_POINT_SUM
else -> TYPE_NORMAL
}
val gradeCountToAverage = subjectElement.select("#avg").text().isNotBlank()
val grades = subjectElement.select(".grade[data-edited]")
val gradesInfo = subjectElement.select(".grade-tip")
val gradeValues = if (grades.isNotEmpty()) {
subjects.select(".avg-$subjectId .grade-tip > p").first()
?.text()?.split('+')?.map {
val split = it.split('*')
val value = split[1].trim().toFloatOrNull()
val weight = value?.let { split[0].trim().toFloatOrNull() } ?: 0f
Pair(value ?: 0f, weight)
} ?: emptyList()
} else emptyList()
grades.forEachIndexed { index, gradeElement ->
val id = Regexes.EDUDZIENNIK_GRADE_ID.find(gradeElement.attr("href"))?.get(1)?.crc32()
?: return@forEachIndexed
val (value, weight) = gradeValues[index]
val name = gradeElement.text().trim().let {
if (it.contains(',') || it.contains('.')) {
val replaced = it.replace(',', '.')
val float = replaced.toFloatOrNull()
if (float != null && float % 1 == 0f) float.toInt().toString()
else it
} else it
}
val info = gradesInfo[index]
val fullName = info.child(0).text().trim()
val columnName = info.child(4).text().trim()
val comment = info.ownText()
val description = columnName + if (comment.isNotBlank()) " - $comment" else null
val teacherName = info.child(1).text()
val teacher = data.getTeacherByLastFirst(teacherName)
val addedDate = info.child(2).text().split(' ').let {
val day = it[0].toInt()
val month = Utils.monthFromName(it[1])
val year = it[2].toInt()
Date(year, month, day).inMillis
}
val color = Regexes.STYLE_CSS_COLOR.find(gradeElement.attr("style"))?.get(1)?.let {
if (it.startsWith('#')) Color.parseColor(it)
else colorFromCssName(it)
} ?: -1
val gradeObject = Grade(
profileId = profileId,
id = id,
name = name,
type = gradeType,
value = value,
weight = if (gradeCountToAverage) weight else 0f,
color = color,
category = fullName,
description = description,
comment = null,
semester = semester,
teacherId = teacher.id,
subjectId = subject.id,
addedDate = addedDate
)
data.gradeList.add(gradeObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_GRADE,
id,
profile.empty,
profile.empty
))
}
val proposed = subjectElement.select(".proposal").firstOrNull()?.text()?.trim()
if (proposed != null && proposed.isNotBlank()) {
val proposedGradeObject = Grade(
profileId = profileId,
id = (-1 * subject.id) - 1,
name = proposed,
type = when (semester) {
1 -> TYPE_SEMESTER1_PROPOSED
else -> TYPE_SEMESTER2_PROPOSED
},
value = proposed.toFloatOrNull() ?: 0f,
weight = 0f,
color = -1,
category = null,
description = null,
comment = null,
semester = semester,
teacherId = -1,
subjectId = subject.id
)
data.gradeList.add(proposedGradeObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_GRADE,
proposedGradeObject.id,
profile.empty,
profile.empty
))
}
val final = subjectElement.select(".final").firstOrNull()?.text()?.trim()
if (final != null && final.isNotBlank()) {
val finalGradeObject = Grade(
profileId = profileId,
id = (-1 * subject.id) - 2,
name = final,
type = when (semester) {
1 -> TYPE_SEMESTER1_FINAL
else -> TYPE_SEMESTER2_FINAL
},
value = final.toFloatOrNull() ?: 0f,
weight = 0f,
color = -1,
category = null,
description = null,
comment = null,
semester = semester,
teacherId = -1,
subjectId = subject.id
)
data.gradeList.add(finalGradeObject)
data.metadataList.add(Metadata(
data.profileId,
Metadata.TYPE_GRADE,
finalGradeObject.id,
profile.empty,
profile.empty
))
}
}
if (!subjects.isNullOrEmpty()) {
data.toRemove.addAll(listOf(
TYPE_NORMAL,
TYPE_POINT_SUM,
TYPE_SEMESTER1_PROPOSED,
TYPE_SEMESTER2_PROPOSED,
TYPE_SEMESTER1_FINAL,
TYPE_SEMESTER2_FINAL
).map {
DataRemoveModel.Grades.semesterWithType(semester, it)
})
}
if (profile.empty && requestSemester == 1 && data.currentSemester == 2) {
requestSemester = null
getGrades()
} else {
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_GRADES, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_GRADES)
}
}
} ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_GRADES) }
}

View File

@ -1,86 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-29
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_HOMEWORK_ID
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
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.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.utils.models.Date
class EdudziennikWebHomework(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
const val TAG = "EdudziennikWebHomework"
}
init { data.profile?.also { profile ->
webGet(TAG, data.courseStudentEndpoint + "Homework", xhr = true) { text ->
val doc = Jsoup.parseBodyFragment("<table>" + text.trim() + "</table>")
if (doc.getElementsByClass("message").text().trim() != "Brak prac domowych") {
doc.getElementsByTag("tr").forEach { homeworkElement ->
val dateElement = homeworkElement.getElementsByClass("date").first()?.child(0) ?: return@forEach
val idStr = EDUDZIENNIK_HOMEWORK_ID.find(dateElement.attr("href"))?.get(1) ?: return@forEach
val id = idStr.crc32()
val date = Date.fromY_m_d(dateElement.text())
val subjectElement = homeworkElement.child(1).child(0)
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEach
val subjectName = subjectElement.text()
val subject = data.getSubject(subjectId.crc32(), subjectName)
val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date)
val startTime = lessons.firstOrNull { it.subjectId == subject.id }?.displayStartTime
val teacherName = homeworkElement.child(2).text()
val teacher = data.getTeacherByFirstLast(teacherName)
val topic = homeworkElement.child(4).text().trim()
val eventObject = Event(
profileId = profileId,
id = id,
date = date,
time = startTime,
topic = topic ?: "",
color = null,
type = Event.TYPE_HOMEWORK,
teacherId = teacher.id,
subjectId = subject.id,
teamId = data.teamClass?.id ?: -1
)
eventObject.attachmentNames = mutableListOf(idStr)
data.eventList.add(eventObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_HOMEWORK,
id,
profile.empty,
profile.empty
))
}
}
data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK))
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK)
}
} ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK) }
}

View File

@ -1,46 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-23
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
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.utils.models.Date
class EdudziennikWebLuckyNumber(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
private const val TAG = "EdudziennikWebLuckyNumber"
}
init { data.profile?.also { profile ->
webGet(TAG, data.schoolEndpoint + "Lucky", xhr = true) { text ->
text.toIntOrNull()?.also { luckyNumber ->
val luckyNumberObject = LuckyNumber(
profileId = profileId,
date = Date.getToday(),
number = luckyNumber
)
data.luckyNumberList.add(luckyNumberObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_LUCKY_NUMBER,
luckyNumberObject.date.value.toLong(),
true,
profile.empty
))
}
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER)
}
} ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER) }
}

View File

@ -1,69 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-1-1
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_NOTE_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_NOTES
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
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
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.utils.models.Date
class EdudziennikWebNotes(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
const val TAG = "EdudziennikWebNotes"
}
init { data.profile?.also { profile ->
webGet(TAG, data.classStudentEndpoint + "RegistryNotesStudent", xhr = true) { text ->
val doc = Jsoup.parseBodyFragment("<table>" + text.trim() + "</table>")
doc.getElementsByTag("tr").forEach { noteElement ->
val dateElement = noteElement.getElementsByClass("date").first()?.child(0) ?: return@forEach
val addedDate = Date.fromY_m_d(dateElement.text()).inMillis
val id = EDUDZIENNIK_NOTE_ID.find(dateElement.attr("href"))?.get(0)?.crc32()
?: return@forEach
val teacherName = noteElement.child(1).text()
val teacher = data.getTeacherByFirstLast(teacherName)
val description = noteElement.child(3).text()
val noticeObject = Notice(
profileId = profileId,
id = id,
type = Notice.TYPE_NEUTRAL,
semester = profile.currentSemester,
text = description,
category = null,
points = null,
teacherId = teacher.id,
addedDate = addedDate
)
data.noticeList.add(noticeObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_NOTICE,
id,
profile.empty,
profile.empty
))
}
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_NOTES, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_NOTES)
}
} ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_NOTES) }
}

View File

@ -1,79 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-23
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import pl.szczodrzynski.edziennik.data.api.ERROR_EDUDZIENNIK_WEB_TEAM_MISSING
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECTS_START
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_START
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.ext.MONTH
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.firstLettersName
import pl.szczodrzynski.edziennik.ext.get
class EdudziennikWebStart(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
private const val TAG = "EdudziennikWebStart"
}
init {
webGet(TAG, data.studentEndpoint + "start") { text ->
getSchoolAndTeam(text)
getSubjects(text)
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_START, MONTH)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_START)
}
}
private fun getSchoolAndTeam(text: String) {
val schoolId = Regexes.EDUDZIENNIK_SCHOOL_DETAIL_ID.find(text)?.get(1)?.trim()
val schoolLongName = Regexes.EDUDZIENNIK_SCHOOL_DETAIL_NAME.find(text)?.get(1)?.trim()
data.schoolId = schoolId
val classId = Regexes.EDUDZIENNIK_CLASS_DETAIL_ID.find(text)?.get(1)?.trim()
val className = Regexes.EDUDZIENNIK_CLASS_DETAIL_NAME.find(text)?.get(1)?.trim()
data.classId = classId
if (classId == null || className == null || schoolId == null || schoolLongName == null) {
data.error(ApiError(TAG, ERROR_EDUDZIENNIK_WEB_TEAM_MISSING)
.withApiResponse(text))
return
}
val schoolName = schoolId.crc32().toString() + schoolLongName.firstLettersName + "_edu"
data.schoolName = schoolName
val teamId = classId.crc32()
val teamCode = "$schoolName:$className"
val teamObject = Team(
data.profileId,
teamId,
className,
Team.TYPE_CLASS,
teamCode,
-1
)
data.teamClass = teamObject
data.teamList.put(teamObject.id, teamObject)
}
private fun getSubjects(text: String) {
EDUDZIENNIK_SUBJECTS_START.findAll(text).forEach {
val id = it[1].trim()
val name = it[2].trim()
data.getSubject(id.crc32(), name)
}
}
}

View File

@ -1,34 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-25
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHERS
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_TEACHERS
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.ext.MONTH
import pl.szczodrzynski.edziennik.ext.get
class EdudziennikWebTeachers(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
private const val TAG = "EdudziennikWebTeachers"
}
init {
webGet(TAG, data.studentAndTeacherClassEndpoint + "grid") { text ->
EDUDZIENNIK_TEACHERS.findAll(text).forEach {
val lastName = it[1].trim()
val firstName = it[2].trim()
data.getTeacher(firstName, lastName)
}
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_TEACHERS, MONTH)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_TEACHERS)
}
}
}

View File

@ -1,150 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-23
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHER_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.LessonRange
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.singleOrNull
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class EdudziennikWebTimetable(override val data: DataEdudziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : EdudziennikWeb(data, lastSync) {
companion object {
private const val TAG = "EdudziennikWebTimetable"
}
init { data.profile?.also { profile ->
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)
webGet(TAG, data.timetableEndpoint + "print?date=$getDate") { text ->
val doc = Jsoup.parse(text)
val dataDays = mutableListOf<Int>()
val dataStart = weekStart.clone()
while (dataStart <= weekEnd) {
dataDays += dataStart.value
dataStart.stepForward(0, 0, 1)
}
val table = doc.select("#Schedule tbody").first()
if (table?.text()?.contains("Brak planu lekcji.") == false) {
table.children().forEach { row ->
val rowElements = row.children()
val lessonNumber = rowElements[0].text().toInt()
val times = rowElements[1].text().split('-')
val startTime = Time.fromH_m(times[0].trim())
val endTime = Time.fromH_m(times[1].trim())
data.lessonRanges.singleOrNull {
it.lessonNumber == lessonNumber && it.startTime == startTime && it.endTime == endTime
} ?: run {
data.lessonRanges.put(lessonNumber, LessonRange(profileId, lessonNumber, startTime, endTime))
}
rowElements.subList(2, rowElements.size).forEachIndexed { index, lesson ->
val course = lesson.select(".course").firstOrNull() ?: return@forEachIndexed
val info = course.select("span > span")
if (info.isEmpty()) return@forEachIndexed
val type = when (course.hasClass("substitute")) {
true -> Lesson.TYPE_CHANGE
else -> Lesson.TYPE_NORMAL
}
/* Getting subject */
val subjectElement = info[0].child(0)
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEachIndexed
val subjectName = subjectElement.text().trim()
val subject = data.getSubject(subjectId.crc32(), subjectName)
/* Getting teacher */
val teacherId = if (info.size >= 2) {
val teacherElement = info[1].child(0)
val teacherLongId = EDUDZIENNIK_TEACHER_ID.find(teacherElement.attr("href"))?.get(1)
val teacherName = teacherElement.text().trim()
data.getTeacherByLastFirst(teacherName, teacherLongId).id
} else null
val lessonObject = Lesson(profileId, -1).also {
it.type = type
it.date = weekStart.clone().stepForward(0, 0, index)
it.lessonNumber = lessonNumber
it.startTime = startTime
it.endTime = endTime
it.subjectId = subject.id
it.teacherId = teacherId
it.teamId = data.teamClass?.id
it.id = it.buildId()
}
data.lessonList.add(lessonObject)
dataDays.remove(lessonObject.date!!.value)
if (type != Lesson.TYPE_NORMAL) {
val seen = profile.empty || lessonObject.date!! < Date.getToday()
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
lessonObject.id,
seen,
seen
))
}
}
}
}
for (day in dataDays) {
val lessonDate = Date.fromValue(day)
data.lessonList += Lesson(profileId, lessonDate.value.toLong()).apply {
type = Lesson.TYPE_NO_LESSONS
date = lessonDate
}
}
d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate")
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS)
onSuccess(ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE)
}
} ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE) }
}

View File

@ -1,67 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-22
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.firstlogin
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_EDUDZIENNIK
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ACCOUNT_NAME_START
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_STUDENTS_START
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.ext.fixName
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.getShortName
import pl.szczodrzynski.edziennik.ext.set
class EdudziennikFirstLogin(val data: DataEdudziennik, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "EdudziennikFirstLogin"
}
private val web = EdudziennikWeb(data, null)
private val profileList = mutableListOf<Profile>()
init {
val loginStoreId = data.loginStore.id
val loginStoreType = LOGIN_TYPE_EDUDZIENNIK
var firstProfileId = loginStoreId
EdudziennikLoginWeb(data) {
web.webGet(TAG, "") { text ->
val accountNameLong = EDUDZIENNIK_ACCOUNT_NAME_START.find(text)?.get(1)?.fixName()
EDUDZIENNIK_STUDENTS_START.findAll(text).forEach {
val studentId = it[1]
val studentNameLong = it[2].fixName()
if (studentId.isBlank() || studentNameLong.isBlank()) return@forEach
val studentNameShort = studentNameLong.getShortName()
val accountName = if (accountNameLong == studentNameLong) null else accountNameLong
val profile = Profile(
firstProfileId++,
loginStoreId,
loginStoreType,
studentNameLong,
data.loginEmail,
studentNameLong,
studentNameShort,
accountName
).apply {
studentData["studentId"] = studentId
}
profileList.add(profile)
}
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
onSuccess()
}
}
}
}

View File

@ -1,97 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-22
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.getUnixDate
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.Utils.d
class EdudziennikLoginWeb(val data: DataEdudziennik, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "EdudziennikLoginWeb"
}
init { run {
if (data.isWebLoginValid()) {
onSuccess()
}
else {
data.app.cookieJar.clear("dziennikel.appspot.com")
if (data.loginEmail.isNotNullNorEmpty() && data.loginPassword.isNotNullNorEmpty()) {
loginWithCredentials()
}
else {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
}
}
}}
private fun loginWithCredentials() {
d(TAG, "Request: Edudziennik/Login/Web - https://dziennikel.appspot.com/login/?next=/")
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (text == null || response == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
val url = response.raw().request().url().toString()
if (!url.contains("Student")) {
when {
text.contains("Wprowadzono nieprawidłową nazwę użytkownika lub hasło.") -> ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN
else -> ERROR_LOGIN_EDUDZIENNIK_WEB_OTHER
}.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withApiResponse(text)
.withResponse(response))
return
}
}
val cookies = data.app.cookieJar.getAll("dziennikel.appspot.com")
val sessionId = cookies["sessionid"]
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID)
.withResponse(response)
.withApiResponse(text))
return
}
data.webSessionId = sessionId
data.webSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45 min */
onSuccess()
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("https://dziennikel.appspot.com/login/?next=/")
.userAgent(EDUDZIENNIK_USER_AGENT)
.contentType("application/x-www-form-urlencoded")
.addParameter("email", data.loginEmail)
.addParameter("password", data.loginPassword)
.addParameter("auth_method", "password")
.addParameter("next", "/")
.post()
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.Librus
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.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -162,6 +163,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {

View File

@ -10,6 +10,7 @@ import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -148,12 +149,23 @@ 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
if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) {
data.requireUserAction(
type = UserActionRequiredEvent.Type.RECAPTCHA,
params = Bundle(
"siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY,
"referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER,
),
errorText = R.string.notification_user_action_required_captcha_librus,
)
return
}
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 ->
@ -163,12 +175,6 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
return
}
}
if (json.getBoolean("captchaRequired") == true) {
data.error(ApiError(TAG, ERROR_CAPTCHA_LIBRUS_PORTAL)
.withResponse(response)
.withApiResponse(json))
return
}
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
}

View File

@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.Mobidzien
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.*
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLogin
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -142,6 +143,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {

View File

@ -12,6 +12,7 @@ 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.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -142,6 +143,10 @@ class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore,
callback.onCompleted()
}
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
callback.onRequiresUserAction(event)
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}

View File

@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateData
import pl.szczodrzynski.edziennik.data.api.edziennik.template.firstlogin.TemplateFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLogin
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -108,6 +109,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
callback.onCompleted()
}
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
callback.onRequiresUserAction(event)
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-11.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_USOS_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
class DataUsos(
app: App,
profile: Profile?,
loginStore: LoginStore,
) : Data(app, profile, loginStore) {
fun isApiLoginValid() = oauthTokenKey != null && oauthTokenSecret != null && oauthTokenIsUser
override fun satisfyLoginMethods() {
loginMethods.clear()
if (isApiLoginValid()) {
loginMethods += LOGIN_METHOD_USOS_API
}
}
override fun generateUserCode() = "$schoolId:${profile?.studentNumber ?: studentId}"
var schoolId: String?
get() { mSchoolId = mSchoolId ?: loginStore.getLoginData("schoolId", null); return mSchoolId }
set(value) { loginStore.putLoginData("schoolId", value); mSchoolId = value }
private var mSchoolId: String? = null
var instanceUrl: String?
get() { mInstanceUrl = mInstanceUrl ?: loginStore.getLoginData("instanceUrl", null); return mInstanceUrl }
set(value) { loginStore.putLoginData("instanceUrl", value); mInstanceUrl = value }
private var mInstanceUrl: String? = null
var oauthLoginResponse: String?
get() { mOauthLoginResponse = mOauthLoginResponse ?: loginStore.getLoginData("oauthLoginResponse", null); return mOauthLoginResponse }
set(value) { loginStore.putLoginData("oauthLoginResponse", value); mOauthLoginResponse = value }
private var mOauthLoginResponse: String? = null
var oauthConsumerKey: String?
get() { mOauthConsumerKey = mOauthConsumerKey ?: loginStore.getLoginData("oauthConsumerKey", null); return mOauthConsumerKey }
set(value) { loginStore.putLoginData("oauthConsumerKey", value); mOauthConsumerKey = value }
private var mOauthConsumerKey: String? = null
var oauthConsumerSecret: String?
get() { mOauthConsumerSecret = mOauthConsumerSecret ?: loginStore.getLoginData("oauthConsumerSecret", null); return mOauthConsumerSecret }
set(value) { loginStore.putLoginData("oauthConsumerSecret", value); mOauthConsumerSecret = value }
private var mOauthConsumerSecret: String? = null
var oauthTokenKey: String?
get() { mOauthTokenKey = mOauthTokenKey ?: loginStore.getLoginData("oauthTokenKey", null); return mOauthTokenKey }
set(value) { loginStore.putLoginData("oauthTokenKey", value); mOauthTokenKey = value }
private var mOauthTokenKey: String? = null
var oauthTokenSecret: String?
get() { mOauthTokenSecret = mOauthTokenSecret ?: loginStore.getLoginData("oauthTokenSecret", null); return mOauthTokenSecret }
set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value }
private var mOauthTokenSecret: String? = null
var oauthTokenIsUser: Boolean
get() { mOauthTokenIsUser = mOauthTokenIsUser ?: loginStore.getLoginData("oauthTokenIsUser", false); return mOauthTokenIsUser ?: false }
set(value) { loginStore.putLoginData("oauthTokenIsUser", value); mOauthTokenIsUser = value }
private var mOauthTokenIsUser: Boolean? = null
var studentId: Int
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
private var mStudentId: Int? = null
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-11.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosData
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin.UsosFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
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.prepare
import pl.szczodrzynski.edziennik.data.api.usosLoginMethods
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
class Usos(
val app: App,
val profile: Profile?,
val loginStore: LoginStore,
val callback: EdziennikCallback,
) : EdziennikInterface {
companion object {
private const val TAG = "Usos"
}
val internalErrorList = mutableListOf<Int>()
val data: DataUsos
init {
data = DataUsos(app, profile, loginStore).apply {
callback = wrapCallback(this@Usos.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(usosLoginMethods, UsosFeatures, featureIds, viewId, onlyEndpoints)
d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}")
d(TAG, "Endpoint IDs: ${data.targetEndpointIds}")
UsosLogin(data) {
UsosData(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) {}
override fun getRecipientList() {}
override fun getEvent(eventFull: EventFull) {}
override fun firstLogin() {
UsosFirstLogin(data) {
completed()
}
}
override fun cancel() {
d(TAG, "Cancelled")
data.cancel()
}
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() {
callback.onCompleted()
}
override fun onRequiresUserAction(event: UserActionRequiredEvent) {
callback.onRequiresUserAction(event)
}
override fun onProgress(step: Float) {
callback.onProgress(step)
}
override fun onStartProgress(stringRes: Int) {
callback.onStartProgress(stringRes)
}
override fun onError(apiError: ApiError) {
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,39 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-11.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.models.Feature
const val ENDPOINT_USOS_API_USER = 7000
const val ENDPOINT_USOS_API_TERMS = 7010
const val ENDPOINT_USOS_API_COURSES = 7020
const val ENDPOINT_USOS_API_TIMETABLE = 7030
val UsosFeatures = listOf(
/*
* Student information
*/
Feature(LOGIN_TYPE_USOS, FEATURE_STUDENT_INFO, listOf(
ENDPOINT_USOS_API_USER to LOGIN_METHOD_USOS_API,
), listOf(LOGIN_METHOD_USOS_API)),
/*
* Terms & courses
*/
Feature(LOGIN_TYPE_USOS, FEATURE_SCHOOL_INFO, listOf(
ENDPOINT_USOS_API_TERMS to LOGIN_METHOD_USOS_API,
), listOf(LOGIN_METHOD_USOS_API)),
Feature(LOGIN_TYPE_USOS, FEATURE_TEAM_INFO, listOf(
ENDPOINT_USOS_API_COURSES to LOGIN_METHOD_USOS_API,
), listOf(LOGIN_METHOD_USOS_API)),
/*
* Timetable
*/
Feature(LOGIN_TYPE_USOS, FEATURE_TIMETABLE, listOf(
ENDPOINT_USOS_API_TIMETABLE to LOGIN_METHOD_USOS_API,
), listOf(LOGIN_METHOD_USOS_API)),
)

View File

@ -0,0 +1,209 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-13.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data
import com.google.gson.JsonArray
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.JsonArrayCallbackHandler
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_MISSING_RESPONSE
import pl.szczodrzynski.edziennik.data.api.SERVER_USER_AGENT
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection.*
import java.util.UUID
open class UsosApi(open val data: DataUsos, open val lastSync: Long?) {
companion object {
private const val TAG = "UsosApi"
}
enum class ResponseType {
OBJECT,
ARRAY,
PLAIN,
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
protected fun JsonObject.getLangString(key: String) =
this.getJsonObject(key)?.getString("pl")
protected fun JsonObject.getLecturerIds(key: String) =
this.getJsonArray(key)?.asJsonObjectList()?.mapNotNull {
val id = it.getLong("id") ?: return@mapNotNull null
val firstName = it.getString("first_name") ?: return@mapNotNull null
val lastName = it.getString("last_name") ?: return@mapNotNull null
data.getTeacher(firstName, lastName, id = id).id
} ?: listOf()
private fun valueToString(value: Any) = when (value) {
is String -> value
is Number -> value.toString()
is List<*> -> listToString(value)
else -> value.toString()
}
private fun listToString(list: List<*>): String {
return list.map {
if (it is Pair<*, *> && it.first is String && it.second is List<*>)
return@map "${it.first}[${listToString(it.second as List<*>)}]"
return@map valueToString(it ?: "")
}.joinToString("|")
}
private fun buildSignature(method: String, url: String, params: Map<String, String>): String {
val query = params.toQueryString()
val signatureString = listOf(
method.uppercase(),
url.urlEncode(),
query.urlEncode(),
).joinToString("&")
val signingKey = listOf(
data.oauthConsumerSecret ?: "",
data.oauthTokenSecret ?: "",
).joinToString("&") { it.urlEncode() }
return signatureString.hmacSHA1(signingKey)
}
fun <T> apiRequest(
tag: String,
service: String,
params: Map<String, Any>? = null,
fields: List<Any>? = null,
responseType: ResponseType,
onSuccess: (data: T, response: Response?) -> Unit,
) {
val url = "${data.instanceUrl}services/$service"
d(tag, "Request: Usos/Api - $url")
val formData = mutableMapOf<String, String>()
if (params != null)
formData.putAll(params.mapValues {
valueToString(it.value)
})
if (fields != null)
formData["fields"] = valueToString(fields)
val auth = mutableMapOf(
"realm" to url,
"oauth_consumer_key" to (data.oauthConsumerKey ?: ""),
"oauth_nonce" to UUID.randomUUID().toString(),
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to currentTimeUnix().toString(),
"oauth_token" to (data.oauthTokenKey ?: ""),
"oauth_version" to "1.0",
)
val signature = buildSignature(
method = "POST",
url = url,
params = formData + auth.filterKeys { it.startsWith("oauth_") },
)
auth["oauth_signature"] = signature
val authString = auth.map {
"""${it.key}="${it.value.urlEncode()}""""
}.joinToString(", ")
Request.builder()
.url(url)
.userAgent(SERVER_USER_AGENT)
.addHeader("Authorization", "OAuth $authString")
.post()
.setTextBody(formData.toQueryString(), MediaTypeUtils.APPLICATION_FORM)
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_UNAUTHORIZED)
.allowErrorCode(HTTP_FORBIDDEN)
.allowErrorCode(HTTP_NOT_FOUND)
.allowErrorCode(HTTP_UNAVAILABLE)
.callback(getCallback(tag, responseType, onSuccess))
.build()
.enqueue()
}
@Suppress("UNCHECKED_CAST")
private fun <T> getCallback(
tag: String,
responseType: ResponseType,
onSuccess: (data: T, response: Response?) -> Unit,
) = when (responseType) {
ResponseType.OBJECT -> object : JsonCallbackHandler() {
override fun onSuccess(data: JsonObject?, response: Response) {
processResponse(tag, response, data as T?, onSuccess)
}
override fun onFailure(response: Response?, throwable: Throwable?) {
processError(tag, response, throwable)
}
}
ResponseType.ARRAY -> object : JsonArrayCallbackHandler() {
override fun onSuccess(data: JsonArray?, response: Response) {
processResponse(tag, response, data as T?, onSuccess)
}
override fun onFailure(response: Response?, throwable: Throwable?) {
processError(tag, response, throwable)
}
}
ResponseType.PLAIN -> object : TextCallbackHandler() {
override fun onSuccess(data: String?, response: Response) {
processResponse(tag, response, data as T?, onSuccess)
}
override fun onFailure(response: Response?, throwable: Throwable?) {
processError(tag, response, throwable)
}
}
}
private fun <T> processResponse(
tag: String,
response: Response,
value: T?,
onSuccess: (data: T, response: Response?) -> Unit,
) {
val errorCode = when {
response.code() == HTTP_UNAUTHORIZED -> {
data.oauthTokenKey = null
data.oauthTokenSecret = null
data.oauthTokenIsUser = false
data.oauthLoginResponse = null
UsosLoginApi(data) { }
return
}
value == null -> ERROR_USOS_API_MISSING_RESPONSE
response.code() == HTTP_OK -> {
onSuccess(value, response)
null
}
else -> response.toErrorCode()
}
if (errorCode != null) {
data.error(tag, errorCode, response, value.toString())
}
}
private fun processError(
tag: String,
response: Response?,
throwable: Throwable?,
) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-13.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.*
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser
import pl.szczodrzynski.edziennik.utils.Utils.d
class UsosData(val data: DataUsos, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "UsosData"
}
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) { endpointId ->
data.progress(data.progressStep)
nextEndpoint(onSuccess)
}
}
private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) {
d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync")
when (endpointId) {
ENDPOINT_USOS_API_USER -> {
data.startProgress(R.string.edziennik_progress_endpoint_student_info)
UsosApiUser(data, lastSync, onSuccess)
}
ENDPOINT_USOS_API_TERMS -> {
data.startProgress(R.string.edziennik_progress_endpoint_school_info)
UsosApiTerms(data, lastSync, onSuccess)
}
ENDPOINT_USOS_API_COURSES -> {
data.startProgress(R.string.edziennik_progress_endpoint_teams)
UsosApiCourses(data, lastSync, onSuccess)
}
ENDPOINT_USOS_API_TIMETABLE -> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
UsosApiTimetable(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-15.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.ext.*
class UsosApiCourses(
override val data: DataUsos,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit,
) : UsosApi(data, lastSync) {
companion object {
const val TAG = "UsosApiCourses"
}
init {
apiRequest<JsonObject>(
tag = TAG,
service = "courses/user",
fields = listOf(
// "terms" to listOf("id", "name", "start_date", "end_date"),
"course_editions" to listOf(
"course_id",
"course_name",
// "term_id",
"user_groups" to listOf(
"course_unit_id",
"group_number",
// "class_type",
"class_type_id",
"lecturers",
),
),
),
responseType = ResponseType.OBJECT,
) { json, response ->
if (!processResponse(json)) {
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest
}
data.setSyncNext(ENDPOINT_USOS_API_COURSES, 2 * DAY)
onSuccess(ENDPOINT_USOS_API_COURSES)
}
}
private fun processResponse(json: JsonObject): Boolean {
// val term = json.getJsonArray("terms")?.firstOrNull() ?: return false
val courseEditions = json.getJsonObject("course_editions")
?.entrySet()
?.flatMap { it.value.asJsonArray }
?.map { it.asJsonObject } ?: return false
var hasValidTeam = false
for (courseEdition in courseEditions) {
val courseId = courseEdition.getString("course_id") ?: continue
val courseName = courseEdition.getLangString("course_name") ?: continue
val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue
for (userGroup in userGroups) {
val courseUnitId = userGroup.getLong("course_unit_id") ?: continue
val groupNumber = userGroup.getInt("group_number") ?: continue
// val classType = userGroup.getLangString("class_type") ?: continue
val classTypeId = userGroup.getString("class_type_id") ?: continue
val lecturers = userGroup.getLecturerIds("lecturers")
data.teamList.put(courseUnitId, Team(
profileId,
courseUnitId,
"${profile?.studentClassName} $classTypeId$groupNumber - $courseName",
2,
"${data.schoolId}:${courseId} $classTypeId$groupNumber",
lecturers.firstOrNull() ?: -1L,
))
hasValidTeam = true
}
}
return hasValidTeam
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-15.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonArray
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.models.Date
class UsosApiTerms(
override val data: DataUsos,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit,
) : UsosApi(data, lastSync) {
companion object {
const val TAG = "UsosApiTerms"
}
init {
apiRequest<JsonArray>(
tag = TAG,
service = "terms/search",
params = mapOf(
"query" to Date.getToday().year.toString(),
),
responseType = ResponseType.ARRAY,
) { json, response ->
if (!processResponse(json)) {
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest
}
data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY)
onSuccess(ENDPOINT_USOS_API_TERMS)
}
}
private fun processResponse(json: JsonArray): Boolean {
val dates = mutableSetOf<Date>()
for (term in json.asJsonObjectList()) {
if (!term.getBoolean("is_active", false))
continue
val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) }
val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) }
if (startDate != null)
dates += startDate
if (finishDate != null)
dates += finishDate
}
val datesSorted = dates.sorted()
if (datesSorted.size != 3)
return false
profile?.studentSchoolYearStart = datesSorted[0].year
profile?.dateSemester1Start = datesSorted[0]
profile?.dateSemester2Start = datesSorted[1]
profile?.dateYearEnd = datesSorted[2]
return true
}
}

View File

@ -0,0 +1,141 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-16.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonArray
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class UsosApiTimetable(
override val data: DataUsos,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit,
) : UsosApi(data, lastSync) {
companion object {
const val TAG = "UsosApiTimetable"
}
init {
val currentWeekStart = Week.getWeekStart()
if (Date.getToday().weekDay > 4)
currentWeekStart.stepForward(0, 0, 7)
val weekStart = data.arguments
?.getString("weekStart")
?.let { Date.fromY_m_d(it) }
?: currentWeekStart
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
apiRequest<JsonArray>(
tag = TAG,
service = "tt/user",
params = mapOf(
"start" to weekStart.stringY_m_d,
"days" to 7,
),
fields = listOf(
"type",
"start_time",
"end_time",
"unit_id",
"course_id",
"course_name",
"lecturer_ids",
"building_id",
"room_number",
"classtype_id",
"group_number",
),
responseType = ResponseType.ARRAY,
) { json, response ->
if (!processResponse(json, weekStart..weekEnd)) {
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest
}
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
data.setSyncNext(ENDPOINT_USOS_API_TIMETABLE, SYNC_ALWAYS)
onSuccess(ENDPOINT_USOS_API_TIMETABLE)
}
}
private fun processResponse(json: JsonArray, syncRange: ClosedRange<Date>): Boolean {
val foundDates = mutableSetOf<Date>()
for (activity in json.asJsonObjectList()) {
val type = activity.getString("type")
if (type !in listOf("classgroup", "classgroup2"))
continue
val startTime = activity.getString("start_time") ?: continue
val endTime = activity.getString("end_time") ?: continue
val unitId = activity.getLong("unit_id", -1)
val courseName = activity.getLangString("course_name") ?: continue
val courseId = activity.getString("course_id") ?: continue
val lecturerIds = activity.getJsonArray("lecturer_ids")?.map { it.asLong }
val buildingId = activity.getString("building_id")
val roomNumber = activity.getString("room_number")
val classTypeId = activity.getString("classtype_id")
val groupNumber = activity.getString("group_number")
val lesson = Lesson(profileId, -1).also {
it.type = Lesson.TYPE_NORMAL
it.date = Date.fromY_m_d(startTime)
it.startTime = Time.fromY_m_d_H_m_s(startTime)
it.endTime = Time.fromY_m_d_H_m_s(endTime)
it.subjectId = data.getSubject(
id = null,
name = courseName,
shortName = courseId,
).id
it.teacherId = lecturerIds?.firstOrNull() ?: -1L
it.teamId = unitId
val groupName = classTypeId?.plus(groupNumber)?.let { s -> "($s)" }
it.classroom = "$buildingId / $roomNumber ${groupName ?: ""}"
it.id = it.buildId()
it.color = when (classTypeId) {
"WYK" -> 0xff0d6091
"CW" -> 0xff54306e
"LAB" -> 0xff772747
"KON" -> 0xff1e5128
"^P?SEM" -> 0xff1e5128 // TODO make it regex
else -> 0xff08534c
}.toInt()
}
lesson.date?.let { foundDates += it }
val seen = profile?.empty != false || lesson.date!! < Date.getToday()
data.lessonList.add(lesson)
if (lesson.type != Lesson.TYPE_NORMAL)
data.metadataList += Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
lesson.id,
seen,
seen,
)
}
val notFoundDates = syncRange.asSequence() - foundDates
for (date in notFoundDates) {
data.lessonList += Lesson(profileId, date.value.toLong()).also {
it.type = Lesson.TYPE_NO_LESSONS
it.date = date
}
}
return true
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-16.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_NO_STUDENT_PROGRAMMES
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.*
class UsosApiUser(
override val data: DataUsos,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit,
) : UsosApi(data, lastSync) {
companion object {
const val TAG = "UsosApiUser"
}
init {
apiRequest<JsonObject>(
tag = TAG,
service = "users/user",
params = mapOf(
"fields" to listOf(
"id",
"first_name",
"last_name",
"student_number",
"student_programmes" to listOf(
"programme" to listOf("id"),
),
),
),
responseType = ResponseType.OBJECT,
) { json, response ->
val programmes = json.getJsonArray("student_programmes")
if (programmes.isNullOrEmpty()) {
data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
.withApiResponse(json)
.withResponse(response))
return@apiRequest
}
val firstName = json.getString("first_name")
val lastName = json.getString("last_name")
val studentName = buildFullName(firstName, lastName)
data.studentId = json.getInt("id") ?: data.studentId
profile?.studentNameLong = studentName
profile?.studentNameShort = studentName.getShortName()
profile?.studentNumber = json.getInt("student_number", -1)
profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id")
profile?.studentClassName?.let {
data.getTeam(
id = null,
name = it,
schoolCode = data.schoolId ?: "",
isTeamClass = true,
)
}
data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY)
onSuccess(ENDPOINT_USOS_API_USER)
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-14.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_NO_STUDENT_PROGRAMMES
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_USOS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi
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.ext.*
class UsosFirstLogin(val data: DataUsos, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "UsosFirstLogin"
}
private val api = UsosApi(data, null)
init {
val loginStoreId = data.loginStore.id
val loginStoreType = LOGIN_TYPE_USOS
var firstProfileId = loginStoreId
UsosLoginApi(data) {
api.apiRequest<JsonObject>(
tag = TAG,
service = "users/user",
params = mapOf(
"fields" to listOf(
"id",
"first_name",
"last_name",
"student_number",
"student_programmes" to listOf(
"programme" to listOf("id"),
),
),
),
responseType = UsosApi.ResponseType.OBJECT,
) { json, response ->
val programmes = json.getJsonArray("student_programmes")
if (programmes.isNullOrEmpty()) {
data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
.withApiResponse(json)
.withResponse(response))
return@apiRequest
}
val firstName = json.getString("first_name")
val lastName = json.getString("last_name")
val studentName = buildFullName(firstName, lastName)
val profile = Profile(
id = firstProfileId++,
loginStoreId = loginStoreId, loginStoreType = loginStoreType,
name = studentName,
subname = data.schoolId,
studentNameLong = studentName,
studentNameShort = studentName.getShortName(),
accountName = null, // student account
studentData = JsonObject(
"studentId" to json.getInt("id"),
),
).also {
it.studentNumber = json.getInt("student_number", -1)
it.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id")
}
EventBus.getDefault().postSticky(
FirstLoginFinishedEvent(listOf(profile), data.loginStore),
)
onSuccess()
}
}
}
}

View File

@ -1,17 +1,17 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-22
* Copyright (c) Kuba Szczodrzyński 2022-10-11.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_EDUDZIENNIK_WEB
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_USOS_API
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.utils.Utils.d
class EdudziennikLogin(val data: DataEdudziennik, val onSuccess: () -> Unit) {
class UsosLogin(val data: DataUsos, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "EdudziennikLogin"
private const val TAG = "UsosLogin"
}
private var cancelled = false
@ -43,11 +43,11 @@ class EdudziennikLogin(val data: DataEdudziennik, val onSuccess: () -> Unit) {
onSuccess(-1)
return
}
Utils.d(TAG, "Using login method $loginMethodId")
d(TAG, "Using login method $loginMethodId")
when (loginMethodId) {
LOGIN_METHOD_EDUDZIENNIK_WEB -> {
data.startProgress(R.string.edziennik_progress_login_edudziennik_web)
EdudziennikLoginWeb(data) { onSuccess(loginMethodId) }
LOGIN_METHOD_USOS_API -> {
data.startProgress(R.string.edziennik_progress_login_usos_api)
UsosLoginApi(data) { onSuccess(loginMethodId) }
}
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-11.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.Utils.d
class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "UsosLoginApi"
}
private val api = UsosApi(data, null)
init {
run {
data.arguments?.getString("oauthLoginResponse")?.let {
data.oauthLoginResponse = it
}
if (data.isApiLoginValid()) {
onSuccess()
} else if (data.oauthLoginResponse != null) {
login()
} else {
authorize()
}
}
}
private fun authorize() {
data.oauthTokenKey = null
data.oauthTokenSecret = null
api.apiRequest<String>(
tag = TAG,
service = "oauth/request_token",
params = mapOf(
"oauth_callback" to USOS_API_OAUTH_REDIRECT_URL,
"scopes" to USOS_API_SCOPES,
),
responseType = UsosApi.ResponseType.PLAIN,
) { text, _ ->
val authorizeData = text.fromQueryString()
data.oauthTokenKey = authorizeData["oauth_token"]
data.oauthTokenSecret = authorizeData["oauth_token_secret"]
data.oauthTokenIsUser = false
val authUrl = "${data.instanceUrl}services/oauth/authorize"
val authParams = mapOf(
"interactivity" to "confirm_user",
"oauth_token" to (data.oauthTokenKey ?: ""),
)
data.requireUserAction(
type = UserActionRequiredEvent.Type.OAUTH,
params = Bundle(
"authorizeUrl" to "$authUrl?${authParams.toQueryString()}",
"redirectUrl" to USOS_API_OAUTH_REDIRECT_URL,
"responseStoreKey" to "oauthLoginResponse",
"extras" to data.loginStore.data.toBundle(),
),
errorText = R.string.notification_user_action_required_oauth_usos,
)
}
}
private fun login() {
d(TAG, "Login to ${data.schoolId} with ${data.oauthLoginResponse}")
val authorizeResponse = data.oauthLoginResponse?.fromQueryString()
?: return // checked in init {}
if (authorizeResponse["oauth_token"] != data.oauthTokenKey) {
// got different token
data.error(ApiError(TAG, ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN)
.withApiResponse(data.oauthLoginResponse))
return
}
val verifier = authorizeResponse["oauth_verifier"]
if (verifier.isNullOrBlank()) {
data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE)
.withApiResponse(data.oauthLoginResponse))
return
}
api.apiRequest<String>(
tag = TAG,
service = "oauth/access_token",
params = mapOf(
"oauth_verifier" to verifier,
),
responseType = UsosApi.ResponseType.PLAIN,
) { text, response ->
val accessData = text.fromQueryString()
data.oauthTokenKey = accessData["oauth_token"]
data.oauthTokenSecret = accessData["oauth_token_secret"]
data.oauthTokenIsUser = data.oauthTokenKey != null && data.oauthTokenSecret != null
data.loginStore.removeLoginData("oauthLoginResponse")
if (!data.oauthTokenIsUser)
data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE)
.withApiResponse(text)
.withResponse(response))
else
onSuccess()
}
}
}

View File

@ -222,15 +222,15 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext }
set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value }
private var mSenderAddressHash: String? = null
var senderAddressHash: String?
get() { mSenderAddressHash = mSenderAddressHash ?: profile?.getStudentData("senderAddressHash", null); return mSenderAddressHash }
set(value) { profile?.putStudentData("senderAddressHash", value) ?: return; mSenderAddressHash = value }
private var mMessageBoxKey: String? = null
var messageBoxKey: String?
get() { mMessageBoxKey = mMessageBoxKey ?: profile?.getStudentData("messageBoxKey", null); return mMessageBoxKey }
set(value) { profile?.putStudentData("messageBoxKey", value) ?: return; mMessageBoxKey = value }
private var mSenderAddressName: String? = null
var senderAddressName: String?
get() { mSenderAddressName = mSenderAddressName ?: profile?.getStudentData("senderAddressName", null); return mSenderAddressName }
set(value) { profile?.putStudentData("senderAddressName", value) ?: return; mSenderAddressName = value }
private var mMessageBoxName: String? = null
var messageBoxName: String?
get() { mMessageBoxName = mMessageBoxName ?: profile?.getStudentData("messageBoxName", null); return mMessageBoxName }
set(value) { profile?.putStudentData("messageBoxName", value) ?: return; mMessageBoxName = value }
val apiUrl: String?
get() {

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -179,6 +180,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback {
return object : EdziennikCallback {
override fun onCompleted() { callback.onCompleted() }
override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) }
override fun onProgress(step: Float) { callback.onProgress(step) }
override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) }
override fun onError(apiError: ApiError) {

View File

@ -12,6 +12,7 @@ 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_ADDRESSBOOK_2 = 3501 // after message boxes (3500)
const val ENDPOINT_VULCAN_HEBE_TIMETABLE = 3020
const val ENDPOINT_VULCAN_HEBE_EXAMS = 3030
const val ENDPOINT_VULCAN_HEBE_GRADES = 3040
@ -19,10 +20,11 @@ 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_TEACHERS = 3110
const val ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER = 3200
const val ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES = 3500
const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3510
const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3520
val VulcanFeatures = listOf(
// timetable
@ -85,6 +87,8 @@ val VulcanFeatures = listOf(
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,
ENDPOINT_VULCAN_HEBE_TEACHERS to LOGIN_METHOD_VULCAN_HEBE
ENDPOINT_VULCAN_HEBE_TEACHERS to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES to LOGIN_METHOD_VULCAN_HEBE,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 to LOGIN_METHOD_VULCAN_HEBE,
), listOf(LOGIN_METHOD_VULCAN_HEBE))
)

View File

@ -21,10 +21,12 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
ENDPOINT_VULCAN_HEBE_MAIN,
ENDPOINT_VULCAN_HEBE_PUSH_CONFIG,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK,
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2,
ENDPOINT_VULCAN_HEBE_TIMETABLE,
ENDPOINT_VULCAN_HEBE_EXAMS,
ENDPOINT_VULCAN_HEBE_HOMEWORK,
ENDPOINT_VULCAN_HEBE_NOTICES,
ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES,
ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX,
ENDPOINT_VULCAN_HEBE_MESSAGES_SENT,
ENDPOINT_VULCAN_HEBE_TEACHERS,
@ -107,6 +109,10 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_addressbook)
VulcanHebeAddressbook(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 -> {
data.startProgress(R.string.edziennik_progress_endpoint_addressbook)
VulcanHebeAddressbook2(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_TEACHERS -> {
data.startProgress(R.string.edziennik_progress_endpoint_teachers)
VulcanHebeTeachers(data, lastSync, onSuccess)
@ -139,6 +145,10 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_attendance)
VulcanHebeAttendance(data, lastSync, onSuccess)
}
ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages)
VulcanHebeMessageBoxes(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)

View File

@ -14,7 +14,6 @@ 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
@ -55,6 +54,15 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
return date.getLong("Timestamp") ?: return default
}
fun buildDateTime(): JsonObject {
return JsonObject(
"Timestamp" to System.currentTimeMillis(),
"Date" to Date.getToday().stringY_m_d,
"DateDisplay" to Date.getToday().stringDmy,
"Time" to Time.getNow().stringHMS,
)
}
fun getDate(json: JsonObject?, key: String): Date? {
val date = json.getJsonObject(key)
return date.getString("Date")?.let { Date.fromY_m_d(it) }
@ -74,6 +82,22 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
return teacherId
}
fun getTeacherRecipient(json: JsonObject): Teacher? {
val globalKey = json.getString("GlobalKey") ?: return null
if (globalKey == data.messageBoxKey)
return null
var name = json.getString("Name") ?: return null
val group = json.getString("Group", "P")
val loginId = "${globalKey};${group};${name}"
val pattern = " - $group - (${data.schoolShort})"
if (name.endsWith(pattern))
name = name.substringBefore(pattern)
val teacher = data.getTeacherByFirstLast(name, loginId)
if (teacher.type == 0)
teacher.type = Teacher.TYPE_OTHER
return teacher
}
fun getSubjectId(json: JsonObject?, key: String): Long? {
val subject = json.getJsonObject(key)
val subjectId = subject.getLong("Id") ?: return null
@ -89,7 +113,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
}
fun getTeamId(json: JsonObject?, key: String): Long? {
val team = json.getJsonObject(key)
val team = json.getJsonObject(key) ?: return null
val teamId = team.getLong("Id")
var teamName = team.getString("Shortcut")
?: team.getString("Name")
@ -104,7 +128,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
}
fun getClassId(json: JsonObject?, key: String): Long? {
val team = json.getJsonObject(key)
val team = json.getJsonObject(key) ?: return null
val teamId = team.getLong("Id")
val teamName = data.profile?.studentClassName
?: team.getString("Name")
@ -148,7 +172,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
fun isCurrentYear(dateTime: Long): Boolean {
return profile?.let { profile ->
return@let dateTime >= profile.dateSemester1Start.inMillis
return@let dateTime >= profile.dateSemester1Start.inMillis - WEEK * MS
} ?: false
}
@ -355,6 +379,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
dateTo: Date? = null,
lastSync: Long? = null,
folder: Int? = null,
messageBox: String? = null,
params: Map<String, String> = mapOf(),
includeFilterType: Boolean = true,
onSuccess: (data: List<JsonObject>, response: Response?) -> Unit
@ -378,6 +403,9 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
query["periodId"] = data.studentSemesterId.toString()
query["pupilId"] = data.studentId.toString()
}
HebeFilterType.BY_MESSAGEBOX -> {
query["box"] = messageBox ?: data.messageBoxKey ?: ""
}
}
if (dateFrom != null)

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
enum class HebeFilterType(val endpoint: String) {
BY_MESSAGEBOX("byBox"),
BY_PUPIL("byPupil"),
BY_PERSON("byPerson"),
BY_PERIOD("byPeriod")

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.core.util.set
import androidx.room.OnConflictStrategy
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ADDRESSBOOK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
@ -44,7 +45,6 @@ class VulcanHebeAddressbook(
) { list, _ ->
list.forEach { person ->
val id = person.getString("Id") ?: return@forEach
val loginId = person.getString("LoginId") ?: return@forEach
val idType = id.split("-")
.getOrNull(0)
@ -69,7 +69,7 @@ class VulcanHebeAddressbook(
idLong,
name,
surname,
loginId
null
).also {
data.teacherList[idLong] = it
}
@ -108,13 +108,14 @@ class VulcanHebeAddressbook(
}
teacher.setTeacherType(personType)
teacher.typeDescription = roleText
if (roleText != null)
teacher.typeDescription = roleText
}
if (teacher.type == 0)
teacher.setTeacherType(typeBase)
}
data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK)
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-2-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import androidx.room.OnConflictStrategy
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
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_STUDENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER
import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.MINUTE
import pl.szczodrzynski.edziennik.ext.getString
class VulcanHebeAddressbook2(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeAddressbook2"
}
init { let {
if (data.messageBoxKey == null) {
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2, 30 * MINUTE)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2)
return@let
}
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK,
HebeFilterType.BY_MESSAGEBOX,
messageBox = data.messageBoxKey,
lastSync = lastSync,
includeFilterType = false
) { list, _ ->
list.forEach { person ->
val teacher = getTeacherRecipient(person) ?: return@forEach
val group = person.getString("Group", "P")
if (teacher.type == TYPE_OTHER) {
teacher.type = when (group) {
"P" -> TYPE_TEACHER // Pracownik
"O" -> TYPE_PARENT // Opiekun
"U" -> TYPE_STUDENT // Uczeń
else -> TYPE_OTHER
}
}
}
data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2, 2 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2)
}
}}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-9-16.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.getString
class VulcanHebeMessageBoxes(
override val data: DataVulcan,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : VulcanHebe(data, lastSync) {
companion object {
const val TAG = "VulcanHebeMessageBoxes"
}
init {
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGEBOX,
lastSync = lastSync
) { list, _ ->
var found = false
for (messageBox in list) {
val name = messageBox.getString("Name") ?: continue
val studentName = profile?.studentNameLong ?: continue
if (!name.contains(studentName))
continue
data.messageBoxKey = messageBox.getString("GlobalKey")
data.messageBoxName = name
found = true
break
}
if (!found && list.isNotEmpty()) {
list.firstOrNull()?.let { messageBox ->
data.messageBoxKey = messageBox.getString("GlobalKey")
data.messageBoxName = messageBox.getString("Name")
}
}
data.setSyncNext(ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES, 7 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES)
}
}
}

View File

@ -4,22 +4,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.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_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
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.data.db.entity.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.navlib.crc16
class VulcanHebeMessages(
override val data: DataVulcan,
@ -27,29 +26,7 @@ class VulcanHebeMessages(
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
const val TAG = "VulcanHebeMessages"
}
fun getMessages(messageType: Int) {
@ -64,17 +41,28 @@ class VulcanHebeMessages(
TYPE_SENT -> ENDPOINT_VULCAN_HEBE_MESSAGES_SENT
else -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX
}
val messageBox = data.messageBoxKey
if (messageBox == null) {
onSuccess(endpointId)
return
}
apiGetList(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES,
HebeFilterType.BY_PERSON,
VULCAN_HEBE_ENDPOINT_MESSAGEBOX_MESSAGES,
HebeFilterType.BY_MESSAGEBOX,
messageBox = data.messageBoxKey,
folder = folder,
lastSync = lastSync
) { list, _ ->
list.forEach { message ->
val id = message.getLong("Id") ?: return@forEach
val uuid = message.getString("Id") ?: return@forEach
val id = Utils.crc32(uuid.toByteArray())
val globalKey = message.getString("GlobalKey", "")
val threadKey = message.getString("ThreadKey", "")
val subject = message.getString("Subject") ?: return@forEach
val body = message.getString("Content") ?: return@forEach
var body = message.getString("Content") ?: return@forEach
val sender = message.getJsonObject("Sender") ?: return@forEach
@ -83,27 +71,45 @@ class VulcanHebeMessages(
if (!isCurrentYear(sentDate)) return@forEach
val senderId = if (messageType == TYPE_RECEIVED)
getTeacherRecipient(sender)?.id
else
null
val meta = mutableMapOf(
"uuid" to uuid,
"globalKey" to globalKey,
"threadKey" to threadKey,
)
val metaString = meta.map { "${it.key}=${it.value}" }.join("&")
body = "[META:${metaString}]" + body
body = body.replace("\n", "<br>")
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,
body = body,
senderId = senderId,
addedDate = sentDate
)
val receivers = message.getJsonArray("Receiver")
?.asJsonObjectList()
?: return@forEach
val receiverReadDate =
if (receivers.size == 1) readDate
else -1
for (receiver in receivers) {
val recipientId = if (messageType == TYPE_SENT)
getTeacherRecipient(receiver)?.id ?: -1
else
-1
val receiverReadDate = receiver.getLong("HasRead", -1)
val messageRecipientObject = MessageRecipient(
profileId,
if (messageType == TYPE_SENT) getPersonId(receiver) else -1,
recipientId,
-1,
receiverReadDate,
id
@ -115,6 +121,9 @@ class VulcanHebeMessages(
?.asJsonObjectList()
?: return@forEach
messageObject.attachmentIds = mutableListOf()
messageObject.attachmentNames = mutableListOf()
messageObject.attachmentSizes = mutableListOf()
for (attachment in attachments) {
val fileName = attachment.getString("Name") ?: continue
val url = attachment.getString("Link") ?: continue

View File

@ -5,7 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_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
@ -23,13 +23,19 @@ class VulcanHebeMessagesChangeStatus(
const val TAG = "VulcanHebeMessagesChangeStatus"
}
init {
init { let {
val messageKey = messageObject.body?.let { data.parseMessageMeta(it) }?.get("uuid") ?: run {
EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess()
return@let
}
apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS,
VULCAN_HEBE_ENDPOINT_MESSAGEBOX_STATUS,
payload = JsonObject(
"MessageId" to messageObject.id,
"LoginId" to data.studentLoginId,
"BoxKey" to data.messageBoxKey,
"MessageKey" to messageKey,
"Status" to 1
)
) { _: Boolean, _ ->
@ -61,5 +67,5 @@ class VulcanHebeMessagesChangeStatus(
EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess()
}
}
}}
}

View File

@ -4,17 +4,20 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_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.Teacher
import pl.szczodrzynski.edziennik.ext.*
import java.util.UUID
class VulcanHebeSendMessage(
override val data: DataVulcan,
@ -28,9 +31,9 @@ class VulcanHebeSendMessage(
}
init {
if (data.senderAddressName == null || data.senderAddressHash == null) {
VulcanHebeMain(data).getStudents(data.profile, null) {
if (data.senderAddressName == null || data.senderAddressHash == null) {
if (data.messageBoxKey == null || data.messageBoxName == null) {
VulcanHebeMessageBoxes(data, 0) {
if (data.messageBoxKey == null || data.messageBoxName == null) {
data.error(TAG, ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY)
}
else {
@ -44,47 +47,64 @@ class VulcanHebeSendMessage(
}
private fun sendMessage() {
val uuid = UUID.randomUUID().toString()
val globalKey = UUID.randomUUID().toString()
val partition = "${data.symbol}-${data.schoolSymbol}"
val recipientsArray = JsonArray()
recipients.forEach { teacher ->
val loginId = teacher.loginId?.split(";", limit = 3) ?: return@forEach
val key = loginId.getOrNull(0) ?: teacher.loginId
val group = loginId.getOrNull(1)
val name = loginId.getOrNull(2)
if (key?.toIntOrNull() != null) {
// raise error for old-format (non-UUID) login IDs
data.error(TAG, ERROR_MESSAGE_NOT_SENT)
return
}
recipientsArray += JsonObject(
"Address" to teacher.fullNameLastFirst,
"LoginId" to (teacher.loginId?.toIntOrNull() ?: return@forEach),
"Initials" to teacher.initialsLastFirst,
"AddressHash" to teacher.fullNameLastFirst.sha1Hex()
"Id" to "${data.messageBoxKey}-${key}",
"Partition" to partition,
"Owner" to data.messageBoxKey,
"GlobalKey" to key,
"Name" to name,
"Group" to group,
"Initials" to "",
"HasRead" to 0,
)
}
val senderName = (profile?.accountName ?: profile?.studentNameLong)
?.swapFirstLastName() ?: ""
val sender = JsonObject(
"Address" to data.senderAddressName,
"LoginId" to data.studentLoginId.toString(),
"Initials" to senderName.getNameInitials(),
"AddressHash" to data.senderAddressHash
"Id" to "0",
"Partition" to partition,
"Owner" to data.messageBoxKey,
"GlobalKey" to data.messageBoxKey,
"Name" to data.messageBoxName,
"Group" to "",
"Initials" to "",
"HasRead" to 0,
)
apiPost(
TAG,
VULCAN_HEBE_ENDPOINT_MESSAGES_SEND,
VULCAN_HEBE_ENDPOINT_MESSAGEBOX_SEND,
payload = JsonObject(
"Status" to 1,
"Sender" to sender,
"DateSent" to null,
"DateRead" to null,
"Content" to text,
"Receiver" to recipientsArray,
"Id" to 0,
"Id" to uuid,
"GlobalKey" to globalKey,
"Partition" to partition,
"ThreadKey" to globalKey, // TODO correct threadKey for reply messages
"Subject" to subject,
"Attachments" to null,
"Self" to null
"Content" to text,
"Status" to 1,
"Owner" to data.messageBoxKey,
"DateSent" to buildDateTime(),
"DateRead" to null,
"Sender" to sender,
"Receiver" to recipientsArray,
"Attachments" to JsonArray(),
)
) { json: JsonObject, _ ->
val messageId = json.getLong("Id")
if (messageId == null) {
// TODO error
return@apiPost
}
) { _: JsonObject, _ ->
// TODO handle errors
VulcanHebeMessages(data, null) {
val message = data.messageList.firstOrNull { it.isSent && it.subject == subject }

View File

@ -45,6 +45,7 @@ class VulcanHebeTeachers(
when (subjectName) {
"Pedagog" -> teacher.setTeacherType(Teacher.TYPE_PEDAGOGUE)
"Dyrektor" -> teacher.setTeacherType(Teacher.TYPE_PRINCIPAL)
else -> {
val subjectId = data.getSubject(null, subjectName).id
if (!teacher.subjects.contains(subjectId))

View File

@ -4,13 +4,16 @@
package pl.szczodrzynski.edziennik.data.api.events
data class UserActionRequiredEvent(val profileId: Int, val type: Int) {
companion object {
const val LOGIN_DATA_MOBIDZIENNIK = 101
const val LOGIN_DATA_LIBRUS = 102
const val LOGIN_DATA_IDZIENNIK = 103
const val LOGIN_DATA_VULCAN = 104
const val LOGIN_DATA_EDUDZIENNIK = 105
const val CAPTCHA_LIBRUS = 202
import android.os.Bundle
data class UserActionRequiredEvent(
val profileId: Int?,
val type: Type,
val params: Bundle,
val errorText: Int,
) {
enum class Type {
RECAPTCHA,
OAUTH,
}
}

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.data.api.interfaces
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.models.Feature
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
@ -14,4 +15,5 @@ import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
*/
interface EdziennikCallback : EndpointCallback {
fun onCompleted()
fun onRequiresUserAction(event: UserActionRequiredEvent)
}

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.models
import android.content.Context
import android.os.Bundle
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
@ -30,6 +31,7 @@ class ApiError(val tag: String, var errorCode: Int) {
var request: Request? = null
var response: Response? = null
var isCritical = true
var params: Bundle? = null
fun withThrowable(throwable: Throwable?): ApiError {
this.throwable = throwable
@ -58,6 +60,11 @@ class ApiError(val tag: String, var errorCode: Int) {
return this
}
fun withParams(bundle: Bundle): ApiError {
this.params = bundle
return this
}
fun getStringText(context: Context): String {
return context.resources.getIdentifier("error_${errorCode}", "string", context.packageName).let {
if (it != 0)

View File

@ -1,5 +1,6 @@
package pl.szczodrzynski.edziennik.data.api.models
import android.os.Bundle
import android.util.LongSparseArray
import android.util.SparseArray
import androidx.core.util.set
@ -11,7 +12,9 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE
import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback
import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META
import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.ext.*
@ -36,7 +39,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
/**
* A callback passed to all [Feature]s and [LoginMethod]s
*/
lateinit var callback: EndpointCallback
lateinit var callback: EdziennikCallback
/**
* A list of [LoginMethod]s *already fulfilled* during this sync.
@ -373,6 +376,15 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
callback.onError(apiError)
}
fun requireUserAction(type: UserActionRequiredEvent.Type, params: Bundle, errorText: Int) {
callback.onRequiresUserAction(UserActionRequiredEvent(
profileId = profile?.id,
type = type,
params = params,
errorText = errorText,
))
}
fun progress(step: Float) {
callback.onProgress(step)
}
@ -437,14 +449,14 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
return team
}
fun getTeacher(firstName: String, lastName: String, loginId: String? = null): Teacher {
fun getTeacher(firstName: String, lastName: String, loginId: String? = null, id: Long? = null): Teacher {
val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" }
return validateTeacher(teacher, firstName, lastName, loginId)
return validateTeacher(teacher, firstName, lastName, loginId, id)
}
fun getTeacher(firstNameChar: Char, lastName: String, loginId: String? = null): Teacher {
val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" }
return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId)
return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId, null)
}
fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher {
@ -452,9 +464,9 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
val teacher = teacherList.singleOrNull { it.fullNameLastFirst == nameLastFirst }
val nameParts = nameLastFirst.split(" ", limit = 2)
return if (nameParts.size == 1)
validateTeacher(teacher, nameParts[0], "", loginId)
validateTeacher(teacher, nameParts[0], "", loginId, null)
else
validateTeacher(teacher, nameParts[1], nameParts[0], loginId)
validateTeacher(teacher, nameParts[1], nameParts[0], loginId, null)
}
fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher {
@ -462,9 +474,9 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
val teacher = teacherList.singleOrNull { it.fullName == nameFirstLast }
val nameParts = nameFirstLast.split(" ", limit = 2)
return if (nameParts.size == 1)
validateTeacher(teacher, nameParts[0], "", loginId)
validateTeacher(teacher, nameParts[0], "", loginId, null)
else
validateTeacher(teacher, nameParts[0], nameParts[1], loginId)
validateTeacher(teacher, nameParts[0], nameParts[1], loginId, null)
}
fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher {
@ -483,17 +495,31 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
getTeacher(nameParts[0][0], nameParts[1], loginId)
}
private fun validateTeacher(teacher: Teacher?, firstName: String, lastName: String, loginId: String?): Teacher {
val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).apply {
id = fullName.crc32()
teacherList[id] = this
private fun validateTeacher(
teacher: Teacher?,
firstName: String,
lastName: String,
loginId: String?,
id: Long?
): Teacher {
val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).also {
it.id = id ?: it.fullName.crc32()
teacherList[it.id] = it
}
return obj.also {
if (loginId != null && it.loginId != null)
if (loginId != null)
it.loginId = loginId
if (firstName.length > 1)
it.name = firstName
it.surname = lastName
}
}
fun parseMessageMeta(body: String): Map<String, String>? {
val match = MESSAGE_META.find(body) ?: return null
return match[1].split("&").associateBy(
{ it.substringBefore("=") },
{ it.substringAfter("=") },
)
}
}

View File

@ -451,9 +451,9 @@ class SzkolnyApi(val app: App) : CoroutineScope {
@Throws(Exception::class)
fun getRealms(registerName: String): List<LoginInfo.Platform> {
val response = api.fsLoginRealms(registerName).execute()
val response = api.platforms(registerName).execute()
if (response.isSuccessful && response.body() != null) {
return response.body()!!
return parseResponse(response)
}
throw SzkolnyApiException(null)
}

View File

@ -45,6 +45,6 @@ interface SzkolnyService {
@GET("registerAvailability")
fun registerAvailability(): Call<ApiResponse<Map<String, RegisterAvailabilityStatus>>>
@GET("https://szkolny-eu.github.io/FSLogin/realms/{registerName}.json")
fun fsLoginRealms(@Path("registerName") registerName: String): Call<List<LoginInfo.Platform>>
@GET("platforms/{registerName}")
fun platforms(@Path("registerName") registerName: String): Call<ApiResponse<List<LoginInfo.Platform>>>
}

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MDw/KY+My+===.$param2".sha256()
return "$param1.MTIzNDU2Nzg5MD5fPgIjyZ===.$param2".sha256()
}
}

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_SER
import pl.szczodrzynski.edziennik.ext.Intent
import pl.szczodrzynski.edziennik.ext.asBoldSpannable
import pl.szczodrzynski.edziennik.ext.concat
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.data.db.entity.Notification as AppNotification
@ -92,7 +93,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
MainActivity::class.java,
"fragmentId" to MainActivity.DRAWER_ITEM_NOTIFICATIONS
)
val summaryIntent = PendingIntent.getActivity(app, app.notificationChannelsManager.data.id, intent, PendingIntent.FLAG_ONE_SHOT)
val summaryIntent = PendingIntent.getActivity(app, app.notificationChannelsManager.data.id, intent, PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag())
// On Nougat or newer - show maximum 8 notifications
// On Marshmallow or older - show maximum 4 notifications

View File

@ -44,7 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.*
TimetableManual::class,
Note::class,
Metadata::class
], version = 97)
], version = 98)
@TypeConverters(
ConverterTime::class,
ConverterDate::class,
@ -185,6 +185,7 @@ abstract class AppDb : RoomDatabase() {
Migration95(),
Migration96(),
Migration97(),
Migration98(),
).allowMainThreadQueries().build()
}
}

View File

@ -109,7 +109,7 @@ abstract class EventDao : BaseDao<Event, EventFull> {
abstract fun removeNotManual(profileId: Int)*/
@RawQuery
abstract fun dontKeepFuture(query: SupportSQLiteQuery?): Long
abstract fun dontKeepFuture(query: SupportSQLiteQuery): Long
@Transaction
open fun dontKeepFuture(profileId: Int, todayDate: Date, filter: String) {

View File

@ -74,6 +74,8 @@ open class Lesson(
@Ignore
var showAsUnseen = false
var color: Int? = null
override fun toString(): String {
return "Lesson(profileId=$profileId, " +
"id=$id, " +

View File

@ -13,6 +13,7 @@ import com.google.gson.JsonObject
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
@Entity(tableName = "notifications")
data class Notification(
@ -98,7 +99,7 @@ data class Notification(
fun getPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, MainActivity::class.java)
fillIntent(intent)
return PendingIntent.getActivity(context, id.toInt(), intent, PendingIntent.FLAG_ONE_SHOT)
return PendingIntent.getActivity(context, id.toInt(), intent, PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag())
}
fun getLargeIcon(): IIcon = when (type) {

View File

@ -233,6 +233,10 @@ open class Profile(
MainActivity.DRAWER_ITEM_GRADES,
MainActivity.DRAWER_ITEM_HOMEWORK
)
LOGIN_TYPE_USOS -> listOf(
MainActivity.DRAWER_ITEM_TIMETABLE,
MainActivity.DRAWER_ITEM_AGENDA
)
else -> listOf(
MainActivity.DRAWER_ITEM_TIMETABLE,
MainActivity.DRAWER_ITEM_AGENDA,

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-16.
*/
package pl.szczodrzynski.edziennik.data.db.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration98 : Migration(97, 98) {
override fun migrate(database: SupportSQLiteDatabase) {
// timetable colors - override color in lesson object
database.execSQL("ALTER TABLE timetable ADD COLUMN color INT DEFAULT NULL;")
}
}

View File

@ -164,7 +164,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
val type = if (event.isHomework) Notification.TYPE_NEW_SHARED_HOMEWORK else Notification.TYPE_NEW_SHARED_EVENT
val notificationFilter = app.config.getFor(event.profileId).sync.notificationFilter
if (!notificationFilter.contains(type) && event.sharedBy != "self") {
if (!notificationFilter.contains(type) && event.sharedBy != "self" && event.date >= Date.getToday()) {
val notification = Notification(
id = Notification.buildId(event.profileId, type, event.id),
title = app.getNotificationTitle(type),

View File

@ -22,6 +22,15 @@ fun Bundle?.getFloat(key: String, defaultValue: Float): Float {
fun Bundle?.getString(key: String, defaultValue: String): String {
return this?.getString(key, defaultValue) ?: defaultValue
}
inline fun <reified E : Enum<E>> Bundle?.getEnum(key: String): E? {
return this?.getString(key)?.let {
try {
enumValueOf<E>(it)
} catch (e: Exception) {
null
}
}
}
fun Bundle?.getIntOrNull(key: String): Int? {
return this?.get(key) as? Int
@ -48,6 +57,7 @@ fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
is Bundle -> putBundle(property.first, property.second as Bundle)
is Parcelable -> putParcelable(property.first, property.second as Parcelable)
is Array<*> -> putParcelableArray(property.first, property.second as Array<out Parcelable>)
is Enum<*> -> putString(property.first, (property.second as Enum<*>).name)
}
}
}

View File

@ -10,6 +10,8 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
fun JsonObject?.get(key: String): JsonElement? = this?.get(key)
@ -93,7 +95,13 @@ fun JsonArray(vararg properties: Any?): JsonArray {
}
}
fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0
@OptIn(ExperimentalContracts::class)
fun JsonArray?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.isEmpty
}
operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o)
operator fun JsonArray.plusAssign(o: String) = this.add(o)
operator fun JsonArray.plusAssign(o: Char) = this.add(o)

View File

@ -4,7 +4,9 @@
package pl.szczodrzynski.edziennik.ext
import android.app.PendingIntent
import android.database.Cursor
import android.os.Build
import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
@ -64,3 +66,9 @@ inline fun <A, B, R> ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? {
}
infix fun Int.hasSet(what: Int) = this and what == what
fun pendingIntentFlag(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return PendingIntent.FLAG_IMMUTABLE
return 0
}

View File

@ -18,6 +18,8 @@ import android.text.style.StyleSpan
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import com.mikepenz.materialdrawer.holder.StringHolder
import java.net.URLDecoder
import java.net.URLEncoder
fun CharSequence?.isNotNullNorEmpty(): Boolean {
return this != null && this.isNotEmpty()
@ -343,3 +345,17 @@ fun Int.toStringHolder() = StringHolder(this)
fun CharSequence.toStringHolder() = StringHolder(this)
fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this)
fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8").replace("+", "%20")
fun String.urlDecode(): String = URLDecoder.decode(this, "UTF-8")
fun Map<String, String>.toQueryString() = this
.map { it.key.urlEncode() to it.value.urlEncode() }
.sortedBy { it.first }
.joinToString("&") { "${it.first}=${it.second}" }
fun String.fromQueryString() = this
.substringAfter('?')
.split("&")
.map { it.split("=") }
.associate { it[0].urlDecode() to it[1].urlDecode() }

View File

@ -7,9 +7,10 @@ package pl.szczodrzynski.edziennik.ext
import android.content.Context
import im.wangchao.mhttp.Response
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.text.SimpleDateFormat
import java.util.*
import java.util.Locale
const val MINUTE = 60L
const val HOUR = 60L*MINUTE
@ -115,3 +116,11 @@ fun Context.getSyncInterval(interval: Int): String {
""
return hoursText?.plus(" $minutesText") ?: minutesText
}
fun ClosedRange<Date>.asSequence(): Sequence<Date> = sequence {
val date = this@asSequence.start.clone()
while (date in this@asSequence) {
yield(date.clone())
date.stepForward(0, 0, 1)
}
}

View File

@ -21,6 +21,7 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.concat
import pl.szczodrzynski.edziennik.ext.formatDate
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import java.util.concurrent.TimeUnit
@ -109,7 +110,7 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
}
val notificationIntent = Intent(app, UpdateDownloaderService::class.java)
val pendingIntent = PendingIntent.getService(app, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val pendingIntent = PendingIntent.getService(app, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag())
val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.updates.key)
.setContentTitle(app.getString(R.string.notification_updates_title))
.setContentText(app.getString(R.string.notification_updates_text, update.versionName))

View File

@ -13,14 +13,16 @@ import pl.szczodrzynski.edziennik.databinding.RecaptchaViewBinding
import pl.szczodrzynski.edziennik.ext.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
class LibrusCaptchaDialog(
class RecaptchaPromptDialog(
activity: AppCompatActivity,
private val siteKey: String,
private val referer: String,
private val onSuccess: (recaptchaCode: String) -> Unit,
private val onFailure: (() -> Unit)?,
private val onCancel: (() -> Unit)?,
onShowListener: ((tag: String) -> Unit)? = null,
onDismissListener: ((tag: String) -> Unit)? = null,
) : BindingDialog<RecaptchaViewBinding>(activity, onShowListener, onDismissListener) {
override val TAG = "LibrusCaptchaDialog"
override val TAG = "RecaptchaPromptDialog"
override fun getTitleRes(): Int? = null
override fun inflate(layoutInflater: LayoutInflater) =
@ -46,8 +48,8 @@ class LibrusCaptchaDialog(
b.progress.visibility = View.VISIBLE
RecaptchaDialog(
activity,
siteKey = "6Lf48moUAAAAAB9ClhdvHr46gRWR-CN31CXQPG2U",
referer = "https://portal.librus.pl/rodzina/login",
siteKey = siteKey,
referer = referer,
onSuccess = { recaptchaCode ->
b.checkbox.background = checkboxBackground
b.checkbox.foreground = checkboxForeground
@ -67,6 +69,6 @@ class LibrusCaptchaDialog(
override fun onDismiss() {
if (!success)
onFailure?.invoke()
onCancel?.invoke()
}
}

View File

@ -8,6 +8,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -17,6 +18,8 @@ import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.databinding.TemplateFragmentBinding
import pl.szczodrzynski.edziennik.ext.addOnPageSelectedListener
import pl.szczodrzynski.edziennik.ui.base.lazypager.FragmentLazyPagerAdapter
import pl.szczodrzynski.edziennik.ui.login.LoginActivity
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
import kotlin.coroutines.CoroutineContext
class LabFragment : Fragment(), CoroutineScope {
@ -26,7 +29,7 @@ class LabFragment : Fragment(), CoroutineScope {
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var activity: AppCompatActivity
private lateinit var b: TemplateFragmentBinding
private val job: Job = Job()
@ -36,11 +39,14 @@ class LabFragment : Fragment(), CoroutineScope {
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
activity = (getActivity() as AppCompatActivity?) ?: return null
context ?: return null
app = activity.application as App
b = TemplateFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
when (activity) {
is MainActivity -> b.refreshLayout.setParent((activity as MainActivity).swipeRefreshLayout)
is LoginActivity -> b.refreshLayout.setParent((activity as LoginActivity).swipeRefreshLayout)
}
b.refreshLayout.isEnabled = false
return b.root
}

View File

@ -9,6 +9,7 @@ import android.os.Process
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.sqlite.db.SimpleSQLiteQuery
import com.chuckerteam.chucker.api.Chucker
@ -35,7 +36,7 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var activity: AppCompatActivity
private lateinit var b: LabFragmentBinding
private val job: Job = Job()
@ -45,7 +46,7 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
activity = (getActivity() as AppCompatActivity?) ?: return null
context ?: return null
app = activity.application as App
b = LabFragmentBinding.inflate(inflater)
@ -55,6 +56,16 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
override fun onPageCreated(): Boolean {
b.app = app
if (app.profile.id == 0) {
b.last10unseen.isVisible = false
b.fullSync.isVisible = false
b.clearProfile.isVisible = false
b.rodo.isVisible = false
b.removeHomework.isVisible = false
b.unarchive.isVisible = false
b.profile.isVisible = false
}
b.last10unseen.onClick {
launch(Dispatchers.Default) {
val events = app.db.eventDao().getAllNow(App.profileId)
@ -139,7 +150,8 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
b.profile += profiles.map { TextInputDropDown.Item(it.id.toLong(), "${it.id} ${it.name} archived ${it.archived}", tag = it) }
b.profile.select(app.profileId.toLong())
b.profile.setOnChangeListener {
activity.loadProfile(it.id.toInt())
if (activity is MainActivity)
(activity as MainActivity).loadProfile(it.id.toInt())
return@setOnChangeListener true
}

View File

@ -8,6 +8,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -22,6 +23,7 @@ import pl.szczodrzynski.edziennik.ext.input
import pl.szczodrzynski.edziennik.ext.set
import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.login.LoginActivity
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext
@ -31,7 +33,7 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var activity: AppCompatActivity
private lateinit var b: TemplateListPageFragmentBinding
private val job: Job = Job()
@ -45,7 +47,7 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
activity = (getActivity() as AppCompatActivity?) ?: return null
context ?: return null
app = activity.application as App
b = TemplateListPageFragmentBinding.inflate(inflater)
@ -142,7 +144,10 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
.show()
}
catch (e: Exception) {
activity.error(ApiError.fromThrowable(TAG, e))
if (activity is MainActivity)
(activity as MainActivity).error(ApiError.fromThrowable(TAG, e))
if (activity is LoginActivity)
(activity as LoginActivity).error(ApiError.fromThrowable(TAG, e))
}
})

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-7.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.TimetableConfigDialogBinding
import pl.szczodrzynski.edziennik.ext.Intent
import pl.szczodrzynski.edziennik.ext.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.base.ConfigDialog
import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment
class TimetableConfigDialog(
activity: AppCompatActivity,
reloadOnDismiss: Boolean = true,
onShowListener: ((tag: String) -> Unit)? = null,
onDismissListener: ((tag: String) -> Unit)? = null,
) : ConfigDialog<TimetableConfigDialogBinding>(
activity,
reloadOnDismiss,
onShowListener,
onDismissListener,
) {
override val TAG = "TimetableConfigDialog"
override fun getTitleRes() = R.string.menu_timetable_config
override fun inflate(layoutInflater: LayoutInflater) =
TimetableConfigDialogBinding.inflate(layoutInflater)
private val profileConfig by lazy { app.config.getFor(app.profileId).ui }
override suspend fun loadConfig() {
b.config = profileConfig
}
override suspend fun saveConfig() {
activity.sendBroadcast(Intent(TimetableFragment.ACTION_RELOAD_PAGES))
}
}

View File

@ -36,6 +36,7 @@ class GradesAdapter(
private const val ITEM_TYPE_EMPTY = 2
private const val ITEM_TYPE_GRADE = 3
private const val ITEM_TYPE_STATS = 4
private const val ITEM_TYPE_UNKNOWN_SUBJECT = 5
const val STATE_CLOSED = 0
const val STATE_OPENED = 1
}
@ -58,6 +59,7 @@ class GradesAdapter(
ITEM_TYPE_EMPTY -> EmptyViewHolder(inflater, parent)
ITEM_TYPE_GRADE -> GradeViewHolder(inflater, parent)
ITEM_TYPE_STATS -> StatsViewHolder(inflater, parent)
ITEM_TYPE_UNKNOWN_SUBJECT -> UnknownSubjectViewHolder(inflater, parent)
else -> throw IllegalArgumentException("Incorrect viewType")
}
}
@ -69,6 +71,7 @@ class GradesAdapter(
is GradesEmpty -> ITEM_TYPE_EMPTY
is Grade -> ITEM_TYPE_GRADE
is GradesStats -> ITEM_TYPE_STATS
is GradesUnknownSubject -> ITEM_TYPE_UNKNOWN_SUBJECT
else -> throw IllegalArgumentException("Incorrect viewType")
}
}
@ -86,7 +89,7 @@ class GradesAdapter(
fun expandModel(model: ExpandableItemModel<*>?, view: View?, notifyAdapter: Boolean = true) {
model ?: return
val position = items.indexOf(model)
var position = items.indexOf(model)
if (position == -1)
return
@ -138,9 +141,16 @@ class GradesAdapter(
else -> model.items
}
if (model is GradesSubject && model.isUnknown) {
position++
items.add(position, GradesUnknownSubject())
if (notifyAdapter) notifyItemInserted(position)
}
position++
model.state = STATE_OPENED
items.addAll(position + 1, subItems.filterNotNull())
if (notifyAdapter) notifyItemRangeInserted(position + 1, subItems.size)
items.addAll(position, subItems.filterNotNull())
if (notifyAdapter) notifyItemRangeInserted(position, subItems.size)
if (model is GradesSubject) {
// auto expand first semester
@ -156,9 +166,10 @@ class GradesAdapter(
else -> semester.grades
}
position++
semester.state = STATE_OPENED
items.addAll(position + 2 + semesterIndex, grades)
if (notifyAdapter) notifyItemRangeInserted(position + 2 + semesterIndex, grades.size)
items.addAll(position + semesterIndex, grades)
if (notifyAdapter) notifyItemRangeInserted(position + semesterIndex, grades.size)
}
}
}
@ -198,6 +209,7 @@ class GradesAdapter(
is EmptyViewHolder -> ITEM_TYPE_EMPTY
is GradeViewHolder -> ITEM_TYPE_GRADE
is StatsViewHolder -> ITEM_TYPE_STATS
is UnknownSubjectViewHolder -> ITEM_TYPE_UNKNOWN_SUBJECT
else -> throw IllegalArgumentException("Incorrect viewType")
}
holder.itemView.setTag(R.string.tag_key_view_type, viewType)
@ -210,6 +222,7 @@ class GradesAdapter(
holder is EmptyViewHolder && item is GradesEmpty -> holder.onBind(activity, app, item, position, this)
holder is GradeViewHolder && item is GradeFull -> holder.onBind(activity, app, item, position, this)
holder is StatsViewHolder && item is GradesStats -> holder.onBind(activity, app, item, position, this)
holder is UnknownSubjectViewHolder && item is GradesUnknownSubject -> holder.onBind(activity, app, item, position, this)
}
if (holder is SemesterViewHolder && item is GradesSemester) {

View File

@ -182,6 +182,7 @@ class GradesListFragment : Fragment(), CoroutineScope {
@Suppress("SuspendFunctionOnCoroutineScope")
private fun processGrades(grades: List<GradeFull>): MutableList<Any> {
val items = mutableListOf<GradesSubject>()
var unknownSubjectItem: GradesSubject? = null
var subjectId = -1L
var semesterNumber = 0
@ -200,17 +201,31 @@ class GradesListFragment : Fragment(), CoroutineScope {
subjectId = grade.subjectId
semesterNumber = 0
subject = items.firstOrNull { it.subjectId == subjectId }
?: GradesSubject(grade.subjectId, grade.subjectLongName ?: "").also {
subject = items.firstOrNull { it.subjectId == subjectId } ?: run {
if (grade.subjectLongName != null) {
return@run GradesSubject(grade.subjectId, grade.subjectLongName!!).also {
items += it
it.semester = 2
}
}
if (unknownSubjectItem == null) {
unknownSubjectItem = GradesSubject(-1, "unknown").also {
items += it
it.semester = 2
it.isUnknown = true
}
}
return@run unknownSubjectItem!!
}
}
if (grade.semester != semesterNumber) {
semesterNumber = grade.semester
semester = subject.semesters.firstOrNull { it.number == semesterNumber }
?: GradesSemester(subject.subjectId, grade.semester).also { subject.semesters += it }
?: GradesSemester(subject.subjectId, grade.semester).also {
subject.semesters += it
it.hideEditor = subject.isUnknown
}
}
grade.showAsUnseen = !grade.seen
@ -221,6 +236,11 @@ class GradesListFragment : Fragment(), CoroutineScope {
semester.hasUnseen = true
}
if (subject.isUnknown) {
// unknown subjects may have final grades (i.e. Mobidziennik)
grade.type = Grade.TYPE_NORMAL
}
when (grade.type) {
Grade.TYPE_SEMESTER1_PROPOSED,
Grade.TYPE_SEMESTER2_PROPOSED -> semester.proposedGrade = grade
@ -255,6 +275,10 @@ class GradesListFragment : Fragment(), CoroutineScope {
val yearlyPoint = mutableListOf<Float>()
for (item in items) {
if (item.isUnknown) {
// do not count averages for "unknown" subjects
continue
}
item.semesters.forEach { sem ->
manager.calculateAverages(sem.averages)
if (sem.number == 1) {

View File

@ -14,6 +14,7 @@ data class GradesSemester(
override var level = 2
var hasUnseen = false
var hideEditor = false
val averages = GradesAverages()
var proposedGrade: GradeFull? = null

View File

@ -15,6 +15,7 @@ data class GradesSubject(
var lastAddedDate = 0L
var semester: Int = 1
var isUnknown = false
var hasUnseen: Boolean = false
get() = field || semesters.any { it.hasUnseen }

View File

@ -0,0 +1,7 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-3-14.
*/
package pl.szczodrzynski.edziennik.ui.grades.models
class GradesUnknownSubject

View File

@ -61,6 +61,8 @@ class SemesterViewHolder(
}
}
b.editButton.isVisible = !item.hideEditor
b.average.text = manager.getAverageString(app, item.averages)
b.proposedGrade.setGrade(item.proposedGrade, manager)
b.finalGrade.setGrade(item.finalGrade, manager)

View File

@ -19,9 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.GradesItemSubjectBinding
import pl.szczodrzynski.edziennik.ext.dp
import pl.szczodrzynski.edziennik.ext.resolveAttr
import pl.szczodrzynski.edziennik.ext.setText
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.ui.grades.GradeView
import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter
import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter.Companion.STATE_CLOSED
@ -41,7 +39,12 @@ class SubjectViewHolder(
val manager = app.gradesManager
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.subjectName.text = item.subjectName
if (!item.isUnknown) {
b.subjectName.text = item.subjectName
}
else {
b.subjectName.text = R.string.grades_subject_unknown.resolveString(activity).asItalicSpannable()
}
b.dropdownIcon.rotation = when (item.state) {
STATE_CLOSED -> 0f
else -> 180f

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-3-14.
*/
package pl.szczodrzynski.edziennik.ui.grades.viewholder
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.GradesItemUnknownSubjectBinding
import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter
import pl.szczodrzynski.edziennik.ui.grades.models.GradesUnknownSubject
class UnknownSubjectViewHolder(
inflater: LayoutInflater,
parent: ViewGroup,
val b: GradesItemUnknownSubjectBinding = GradesItemUnknownSubjectBinding.inflate(inflater, parent, false)
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<GradesUnknownSubject, GradesAdapter> {
companion object {
private const val TAG = "UnknownSubjectViewHolder"
}
override fun onBind(activity: AppCompatActivity, app: App, item: GradesUnknownSubject, position: Int, adapter: GradesAdapter) {
}
}

View File

@ -81,7 +81,8 @@ class HomeFragment : Fragment(), CoroutineScope {
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val manager
get() = app.permissionManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
@ -96,6 +97,10 @@ class HomeFragment : Fragment(), CoroutineScope {
if (!isAdded)
return
if (!manager.isNotificationPermissionGranted) {
manager.requestNotificationsPermission(activity, 0, false){}
}
activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_add_remove_cards)
@ -198,7 +203,7 @@ class HomeFragment : Fragment(), CoroutineScope {
}
}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle): Boolean {
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
val fromPosition: Int = b.list.getChildLayoutPosition(host)
if (action == R.id.move_card_down_action) {
swapCards(fromPosition, fromPosition + 1, adapter)

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